Compare commits
7 Commits
custom-dem
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
4140e9fddd | |
|
|
36bcfbf142 | |
|
|
3fd50feac8 | |
|
|
0bb2f95078 | |
|
|
d30a97e317 | |
|
|
ee3809670b | |
|
|
5d726d51ab |
File diff suppressed because it is too large
Load Diff
|
|
@ -12,6 +12,9 @@
|
|||
"dependencies": {
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"@hookform/resolvers": "^3.3.2",
|
||||
"@mantine/core": "^8.2.1",
|
||||
"@mantine/hooks": "^8.2.1",
|
||||
"@mantine/notifications": "^8.2.1",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
"@radix-ui/react-context-menu": "^2.1.5",
|
||||
|
|
|
|||
|
|
@ -1,167 +1,11 @@
|
|||
import { useCallback, useEffect, useRef } from "react"
|
||||
import { UploadDraw } from "./features/upload-draw";
|
||||
|
||||
import useInputImage from "@/hooks/useInputImage"
|
||||
import { keepGUIAlive } from "@/lib/utils"
|
||||
import { getServerConfig } from "@/lib/api"
|
||||
import Header from "@/components/Header"
|
||||
import Workspace from "@/components/Workspace"
|
||||
import FileSelect from "@/components/FileSelect"
|
||||
import { Toaster } from "./components/ui/toaster"
|
||||
import { useStore } from "./lib/states"
|
||||
import { useWindowSize } from "react-use"
|
||||
|
||||
const SUPPORTED_FILE_TYPE = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
"image/bmp",
|
||||
"image/tiff",
|
||||
]
|
||||
function Home() {
|
||||
const [file, updateAppState, setServerConfig, setFile] = useStore((state) => [
|
||||
state.file,
|
||||
state.updateAppState,
|
||||
state.setServerConfig,
|
||||
state.setFile,
|
||||
])
|
||||
|
||||
const userInputImage = useInputImage()
|
||||
|
||||
const windowSize = useWindowSize()
|
||||
|
||||
useEffect(() => {
|
||||
if (userInputImage) {
|
||||
setFile(userInputImage)
|
||||
}
|
||||
}, [userInputImage, setFile])
|
||||
|
||||
useEffect(() => {
|
||||
updateAppState({ windowSize })
|
||||
}, [windowSize])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchServerConfig = async () => {
|
||||
const serverConfig = await getServerConfig()
|
||||
setServerConfig(serverConfig)
|
||||
if (serverConfig.isDesktop) {
|
||||
// Keeping GUI Window Open
|
||||
keepGUIAlive()
|
||||
}
|
||||
}
|
||||
fetchServerConfig()
|
||||
}, [])
|
||||
|
||||
const dragCounter = useRef(0)
|
||||
|
||||
const handleDrag = useCallback((event: any) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}, [])
|
||||
|
||||
const handleDragIn = useCallback((event: any) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
dragCounter.current += 1
|
||||
}, [])
|
||||
|
||||
const handleDragOut = useCallback((event: any) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
dragCounter.current -= 1
|
||||
if (dragCounter.current > 0) return
|
||||
}, [])
|
||||
|
||||
const handleDrop = useCallback((event: any) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (event.dataTransfer.files && event.dataTransfer.files.length > 0) {
|
||||
if (event.dataTransfer.files.length > 1) {
|
||||
// setToastState({
|
||||
// open: true,
|
||||
// desc: "Please drag and drop only one file",
|
||||
// state: "error",
|
||||
// duration: 3000,
|
||||
// })
|
||||
} else {
|
||||
const dragFile = event.dataTransfer.files[0]
|
||||
const fileType = dragFile.type
|
||||
if (SUPPORTED_FILE_TYPE.includes(fileType)) {
|
||||
setFile(dragFile)
|
||||
} else {
|
||||
// setToastState({
|
||||
// open: true,
|
||||
// desc: "Please drag and drop an image file",
|
||||
// state: "error",
|
||||
// duration: 3000,
|
||||
// })
|
||||
}
|
||||
}
|
||||
event.dataTransfer.clearData()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const onPaste = useCallback((event: any) => {
|
||||
// TODO: when sd side panel open, ctrl+v not work
|
||||
// https://htmldom.dev/paste-an-image-from-the-clipboard/
|
||||
if (!event.clipboardData) {
|
||||
return
|
||||
}
|
||||
const clipboardItems = event.clipboardData.items
|
||||
const items: DataTransferItem[] = [].slice
|
||||
.call(clipboardItems)
|
||||
.filter((item: DataTransferItem) => {
|
||||
// Filter the image items only
|
||||
return item.type.indexOf("image") !== -1
|
||||
})
|
||||
|
||||
if (items.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
// TODO: add confirm dialog
|
||||
|
||||
const item = items[0]
|
||||
// Get the blob of image
|
||||
const blob = item.getAsFile()
|
||||
if (blob) {
|
||||
setFile(blob)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("dragenter", handleDragIn)
|
||||
window.addEventListener("dragleave", handleDragOut)
|
||||
window.addEventListener("dragover", handleDrag)
|
||||
window.addEventListener("drop", handleDrop)
|
||||
window.addEventListener("paste", onPaste)
|
||||
return function cleanUp() {
|
||||
window.removeEventListener("dragenter", handleDragIn)
|
||||
window.removeEventListener("dragleave", handleDragOut)
|
||||
window.removeEventListener("dragover", handleDrag)
|
||||
window.removeEventListener("drop", handleDrop)
|
||||
window.removeEventListener("paste", onPaste)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-between w-full bg-[radial-gradient(circle_at_1px_1px,_#8e8e8e8e_1px,_transparent_0)] [background-size:20px_20px] bg-repeat">
|
||||
<Toaster />
|
||||
<Header />
|
||||
<Workspace />
|
||||
{!file ? (
|
||||
<FileSelect
|
||||
onSelection={async (f) => {
|
||||
setFile(f)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</main>
|
||||
)
|
||||
return (
|
||||
<div>
|
||||
<UploadDraw />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Home
|
||||
export default Home;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,71 +1,71 @@
|
|||
import { useState } from "react"
|
||||
import useResolution from "@/hooks/useResolution"
|
||||
import { useState } from "react";
|
||||
import useResolution from "@/hooks/useResolution";
|
||||
|
||||
type FileSelectProps = {
|
||||
onSelection: (file: File) => void
|
||||
}
|
||||
onSelection: (file: File) => void;
|
||||
};
|
||||
|
||||
export default function FileSelect(props: FileSelectProps) {
|
||||
const { onSelection } = props
|
||||
const { onSelection } = props;
|
||||
|
||||
const [uploadElemId] = useState(`file-upload-${Math.random().toString()}`)
|
||||
const [uploadElemId] = useState(`file-upload-${Math.random().toString()}`);
|
||||
|
||||
const resolution = useResolution()
|
||||
const resolution = useResolution();
|
||||
|
||||
function onFileSelected(file: File) {
|
||||
if (!file) {
|
||||
return
|
||||
}
|
||||
// Skip non-image files
|
||||
const isImage = file.type.match("image.*")
|
||||
if (!isImage) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
// Check if file is larger than 20mb
|
||||
if (file.size > 20 * 1024 * 1024) {
|
||||
throw new Error("file too large")
|
||||
}
|
||||
onSelection(file)
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line
|
||||
alert(`error: ${(e as any).message}`)
|
||||
}
|
||||
}
|
||||
function onFileSelected(file: File) {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
// Skip non-image files
|
||||
const isImage = file.type.match("image.*");
|
||||
if (!isImage) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Check if file is larger than 20mb
|
||||
if (file.size > 20 * 1024 * 1024) {
|
||||
throw new Error("file too large");
|
||||
}
|
||||
onSelection(file);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line
|
||||
alert(`error: ${(e as any).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute flex w-screen h-screen justify-center items-center pointer-events-none">
|
||||
<label
|
||||
htmlFor={uploadElemId}
|
||||
className="grid bg-background border-[2px] border-[dashed] rounded-lg min-w-[600px] hover:bg-primary hover:text-primary-foreground pointer-events-auto"
|
||||
>
|
||||
<div
|
||||
className="grid p-16 w-full h-full"
|
||||
onDragOver={(ev) => {
|
||||
ev.stopPropagation()
|
||||
ev.preventDefault()
|
||||
}}
|
||||
>
|
||||
<input
|
||||
className="hidden"
|
||||
id={uploadElemId}
|
||||
name={uploadElemId}
|
||||
type="file"
|
||||
onChange={(ev) => {
|
||||
const file = ev.currentTarget.files?.[0]
|
||||
if (file) {
|
||||
onFileSelected(file)
|
||||
}
|
||||
}}
|
||||
accept="image/png, image/jpeg"
|
||||
/>
|
||||
<p className="text-center">
|
||||
{resolution === "desktop"
|
||||
? "Click here or drag an image file"
|
||||
: "Tap here to load your picture"}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="absolute flex w-screen h-screen justify-center items-center pointer-events-none">
|
||||
<label
|
||||
htmlFor={uploadElemId}
|
||||
className="grid bg-background border-[2px] border-[dashed] rounded-lg min-w-[600px] hover:bg-primary hover:text-primary-foreground pointer-events-auto"
|
||||
>
|
||||
<div
|
||||
className="grid p-16 w-full h-full"
|
||||
onDragOver={(ev) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}}
|
||||
>
|
||||
<input
|
||||
className="hidden"
|
||||
id={uploadElemId}
|
||||
name={uploadElemId}
|
||||
type="file"
|
||||
onChange={(ev) => {
|
||||
const file = ev.currentTarget.files?.[0];
|
||||
if (file) {
|
||||
onFileSelected(file);
|
||||
}
|
||||
}}
|
||||
accept="image/png, image/jpeg"
|
||||
/>
|
||||
<p className="text-center">
|
||||
{resolution === "desktop"
|
||||
? "Click here or drag an image file"
|
||||
: "Tap here to load your picture"}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,198 +1,73 @@
|
|||
import { PlayIcon } from "@radix-ui/react-icons"
|
||||
import { useState } from "react"
|
||||
import { IconButton, ImageUploadButton } from "@/components/ui/button"
|
||||
import Shortcuts from "@/components/Shortcuts"
|
||||
import { useImage } from "@/hooks/useImage"
|
||||
import { IconButton, ImageUploadButton } from "@/components/ui/button";
|
||||
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"
|
||||
import PromptInput from "./PromptInput"
|
||||
import { RotateCw, Image, Upload } from "lucide-react"
|
||||
import FileManager from "./FileManager"
|
||||
import { getMediaFile } from "@/lib/api"
|
||||
import { useStore } from "@/lib/states"
|
||||
import SettingsDialog from "./Settings"
|
||||
import { cn, fileToImage } from "@/lib/utils"
|
||||
import Coffee from "./Coffee"
|
||||
import { useToast } from "./ui/use-toast"
|
||||
import PromptInput from "./PromptInput";
|
||||
import { RotateCw, Image } from "lucide-react";
|
||||
import { useStore } from "@/lib/states";
|
||||
|
||||
const Header = () => {
|
||||
const [
|
||||
file,
|
||||
customMask,
|
||||
isInpainting,
|
||||
serverConfig,
|
||||
runMannually,
|
||||
enableUploadMask,
|
||||
model,
|
||||
setFile,
|
||||
setCustomFile,
|
||||
runInpainting,
|
||||
showPrevMask,
|
||||
hidePrevMask,
|
||||
imageHeight,
|
||||
imageWidth,
|
||||
] = useStore((state) => [
|
||||
state.file,
|
||||
state.customMask,
|
||||
state.isInpainting,
|
||||
state.serverConfig,
|
||||
state.runMannually(),
|
||||
state.settings.enableUploadMask,
|
||||
state.settings.model,
|
||||
state.setFile,
|
||||
state.setCustomFile,
|
||||
state.runInpainting,
|
||||
state.showPrevMask,
|
||||
state.hidePrevMask,
|
||||
state.imageHeight,
|
||||
state.imageWidth,
|
||||
])
|
||||
const [
|
||||
file,
|
||||
isInpainting,
|
||||
model,
|
||||
setFile,
|
||||
runInpainting,
|
||||
showPrevMask,
|
||||
hidePrevMask,
|
||||
] = useStore((state) => [
|
||||
state.file,
|
||||
state.isInpainting,
|
||||
state.settings.model,
|
||||
state.setFile,
|
||||
state.runInpainting,
|
||||
state.showPrevMask,
|
||||
state.hidePrevMask,
|
||||
]);
|
||||
|
||||
const { toast } = useToast()
|
||||
const [maskImage, maskImageLoaded] = useImage(customMask)
|
||||
const [openMaskPopover, setOpenMaskPopover] = useState(false)
|
||||
const handleRerunLastMask = () => {
|
||||
runInpainting();
|
||||
};
|
||||
|
||||
const handleRerunLastMask = () => {
|
||||
runInpainting()
|
||||
}
|
||||
const onRerunMouseEnter = () => {
|
||||
showPrevMask();
|
||||
};
|
||||
|
||||
const onRerunMouseEnter = () => {
|
||||
showPrevMask()
|
||||
}
|
||||
const onRerunMouseLeave = () => {
|
||||
hidePrevMask();
|
||||
};
|
||||
|
||||
const onRerunMouseLeave = () => {
|
||||
hidePrevMask()
|
||||
}
|
||||
return (
|
||||
<header className="h-[60px] px-6 py-4 absolute top-[0] flex justify-between items-center w-full z-20 border-b backdrop-filter backdrop-blur-md bg-background/70">
|
||||
<div className="flex items-center gap-1">
|
||||
<ImageUploadButton
|
||||
disabled={isInpainting}
|
||||
tooltip="Upload image"
|
||||
onFileUpload={(file) => {
|
||||
setFile(file);
|
||||
}}
|
||||
>
|
||||
<Image />
|
||||
</ImageUploadButton>
|
||||
|
||||
return (
|
||||
<header className="h-[60px] px-6 py-4 absolute top-[0] flex justify-between items-center w-full z-20 border-b backdrop-filter backdrop-blur-md bg-background/70">
|
||||
<div className="flex items-center gap-1">
|
||||
{serverConfig.enableFileManager ? (
|
||||
<FileManager
|
||||
photoWidth={512}
|
||||
onPhotoClick={async (tab: string, filename: string) => {
|
||||
try {
|
||||
const newFile = await getMediaFile(tab, filename)
|
||||
setFile(newFile)
|
||||
} catch (e: any) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
description: e.message ? e.message : e.toString(),
|
||||
})
|
||||
return
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{file && !model.need_prompt ? (
|
||||
<IconButton
|
||||
disabled={isInpainting}
|
||||
tooltip="Rerun previous mask"
|
||||
onClick={handleRerunLastMask}
|
||||
onMouseEnter={onRerunMouseEnter}
|
||||
onMouseLeave={onRerunMouseLeave}
|
||||
>
|
||||
<RotateCw />
|
||||
</IconButton>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ImageUploadButton
|
||||
disabled={isInpainting}
|
||||
tooltip="Upload image"
|
||||
onFileUpload={(file) => {
|
||||
setFile(file)
|
||||
}}
|
||||
>
|
||||
<Image />
|
||||
</ImageUploadButton>
|
||||
{model.need_prompt ? <PromptInput /> : <></>}
|
||||
|
||||
<div
|
||||
className={cn([
|
||||
"flex items-center gap-1",
|
||||
file && enableUploadMask ? "visible" : "hidden",
|
||||
])}
|
||||
>
|
||||
<ImageUploadButton
|
||||
disabled={isInpainting}
|
||||
tooltip="Upload custom mask"
|
||||
onFileUpload={async (file) => {
|
||||
let newCustomMask: HTMLImageElement | null = null
|
||||
try {
|
||||
newCustomMask = await fileToImage(file)
|
||||
} catch (e: any) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
description: e.message ? e.message : e.toString(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (
|
||||
newCustomMask.naturalHeight !== imageHeight ||
|
||||
newCustomMask.naturalWidth !== imageWidth
|
||||
) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
description: `The size of the mask must same as image: ${imageWidth}x${imageHeight}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
<div className="flex gap-1"></div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
setCustomFile(file)
|
||||
if (!runMannually) {
|
||||
runInpainting()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Upload />
|
||||
</ImageUploadButton>
|
||||
|
||||
{customMask ? (
|
||||
<Popover open={openMaskPopover}>
|
||||
<PopoverTrigger
|
||||
className="btn-primary side-panel-trigger"
|
||||
onMouseEnter={() => setOpenMaskPopover(true)}
|
||||
onMouseLeave={() => setOpenMaskPopover(false)}
|
||||
style={{
|
||||
visibility: customMask ? "visible" : "hidden",
|
||||
outline: "none",
|
||||
}}
|
||||
onClick={() => {
|
||||
if (customMask) {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<IconButton tooltip="Run custom mask">
|
||||
<PlayIcon />
|
||||
</IconButton>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
{maskImageLoaded ? (
|
||||
<img src={maskImage.src} alt="Custom mask" />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{file && !model.need_prompt ? (
|
||||
<IconButton
|
||||
disabled={isInpainting}
|
||||
tooltip="Rerun previous mask"
|
||||
onClick={handleRerunLastMask}
|
||||
onMouseEnter={onRerunMouseEnter}
|
||||
onMouseLeave={onRerunMouseLeave}
|
||||
>
|
||||
<RotateCw />
|
||||
</IconButton>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{model.need_prompt ? <PromptInput /> : <></>}
|
||||
|
||||
<div className="flex gap-1">
|
||||
<Coffee />
|
||||
<Shortcuts />
|
||||
{serverConfig.disableModelSwitch ? <></> : <SettingsDialog />}
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
export default Header;
|
||||
|
|
|
|||
|
|
@ -1,39 +1,31 @@
|
|||
import { useEffect } from "react"
|
||||
import Editor from "./Editor"
|
||||
import { currentModel } from "@/lib/api"
|
||||
import { useStore } from "@/lib/states"
|
||||
import ImageSize from "./ImageSize"
|
||||
import Plugins from "./Plugins"
|
||||
import { InteractiveSeg } from "./InteractiveSeg"
|
||||
import SidePanel from "./SidePanel"
|
||||
import DiffusionProgress from "./DiffusionProgress"
|
||||
import { useEffect } from "react";
|
||||
import Editor from "./Editor";
|
||||
import { currentModel } from "@/lib/api";
|
||||
import { useStore } from "@/lib/states";
|
||||
import ImageSize from "./ImageSize";
|
||||
|
||||
const Workspace = () => {
|
||||
const [file, updateSettings] = useStore((state) => [
|
||||
state.file,
|
||||
state.updateSettings,
|
||||
])
|
||||
const [file, updateSettings] = useStore((state) => [
|
||||
state.file,
|
||||
state.updateSettings,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCurrentModel = async () => {
|
||||
const model = await currentModel()
|
||||
updateSettings({ model })
|
||||
}
|
||||
fetchCurrentModel()
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
const fetchCurrentModel = async () => {
|
||||
const model = await currentModel();
|
||||
updateSettings({ model });
|
||||
};
|
||||
fetchCurrentModel();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-3 absolute top-[68px] left-[24px] items-center">
|
||||
<Plugins />
|
||||
<ImageSize />
|
||||
</div>
|
||||
<InteractiveSeg />
|
||||
<DiffusionProgress />
|
||||
<SidePanel />
|
||||
{file ? <Editor file={file} /> : <></>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-3 absolute top-[68px] left-[24px] items-center">
|
||||
<ImageSize />
|
||||
</div>
|
||||
{file ? <Editor file={file} /> : <></>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Workspace
|
||||
export default Workspace;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,177 @@
|
|||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { useStore } from "./lib/states";
|
||||
|
||||
import useInputImage from "./hooks/useInputImage";
|
||||
import { keepGUIAlive } from "./lib/utils";
|
||||
import { getServerConfig } from "./lib/api";
|
||||
|
||||
import Header from "./components/Header";
|
||||
import FileSelect from "./components/FileSelect";
|
||||
import Editor from "./components/Editor";
|
||||
import Plugins from "./components/Plugins";
|
||||
import ImageSize from "./components/ImageSize";
|
||||
|
||||
import { useWindowSize } from "react-use";
|
||||
|
||||
const SUPPORTED_FILE_TYPE = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
"image/bmp",
|
||||
"image/tiff",
|
||||
];
|
||||
|
||||
const UploadDraw = () => {
|
||||
const [file, updateAppState, setServerConfig, setFile] = useStore(
|
||||
(state) => [
|
||||
state.file,
|
||||
state.updateAppState,
|
||||
state.setServerConfig,
|
||||
state.setFile,
|
||||
],
|
||||
);
|
||||
|
||||
const userInputImage = useInputImage();
|
||||
|
||||
const windowSize = useWindowSize();
|
||||
|
||||
useEffect(() => {
|
||||
if (userInputImage) {
|
||||
setFile(userInputImage);
|
||||
}
|
||||
}, [userInputImage, setFile]);
|
||||
|
||||
useEffect(() => {
|
||||
updateAppState({ windowSize });
|
||||
}, [windowSize]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchServerConfig = async () => {
|
||||
const serverConfig = await getServerConfig();
|
||||
setServerConfig(serverConfig);
|
||||
if (serverConfig.isDesktop) {
|
||||
// Keeping GUI Window Open
|
||||
keepGUIAlive();
|
||||
}
|
||||
};
|
||||
fetchServerConfig();
|
||||
}, []);
|
||||
|
||||
const dragCounter = useRef(0);
|
||||
|
||||
const handleDrag = useCallback((event: any) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const handleDragIn = useCallback((event: any) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
dragCounter.current += 1;
|
||||
}, []);
|
||||
|
||||
const handleDragOut = useCallback((event: any) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
dragCounter.current -= 1;
|
||||
if (dragCounter.current > 0) return;
|
||||
}, []);
|
||||
|
||||
const handleDrop = useCallback((event: any) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (event.dataTransfer.files && event.dataTransfer.files.length > 0) {
|
||||
if (event.dataTransfer.files.length > 1) {
|
||||
// setToastState({
|
||||
// open: true,
|
||||
// desc: "Please drag and drop only one file",
|
||||
// state: "error",
|
||||
// duration: 3000,
|
||||
// })
|
||||
} else {
|
||||
const dragFile = event.dataTransfer.files[0];
|
||||
const fileType = dragFile.type;
|
||||
if (SUPPORTED_FILE_TYPE.includes(fileType)) {
|
||||
setFile(dragFile);
|
||||
} else {
|
||||
// setToastState({
|
||||
// open: true,
|
||||
// desc: "Please drag and drop an image file",
|
||||
// state: "error",
|
||||
// duration: 3000,
|
||||
// })
|
||||
}
|
||||
}
|
||||
event.dataTransfer.clearData();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onPaste = useCallback((event: any) => {
|
||||
// TODO: when sd side panel open, ctrl+v not work
|
||||
// https://htmldom.dev/paste-an-image-from-the-clipboard/
|
||||
if (!event.clipboardData) {
|
||||
return;
|
||||
}
|
||||
const clipboardItems = event.clipboardData.items;
|
||||
const items: DataTransferItem[] = [].slice
|
||||
.call(clipboardItems)
|
||||
.filter((item: DataTransferItem) => {
|
||||
// Filter the image items only
|
||||
return item.type.indexOf("image") !== -1;
|
||||
});
|
||||
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// TODO: add confirm dialog
|
||||
|
||||
const item = items[0];
|
||||
// Get the blob of image
|
||||
const blob = item.getAsFile();
|
||||
if (blob) {
|
||||
setFile(blob);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("dragenter", handleDragIn);
|
||||
window.addEventListener("dragleave", handleDragOut);
|
||||
window.addEventListener("dragover", handleDrag);
|
||||
window.addEventListener("drop", handleDrop);
|
||||
window.addEventListener("paste", onPaste);
|
||||
return function cleanUp() {
|
||||
window.removeEventListener("dragenter", handleDragIn);
|
||||
window.removeEventListener("dragleave", handleDragOut);
|
||||
window.removeEventListener("dragover", handleDrag);
|
||||
window.removeEventListener("drop", handleDrop);
|
||||
window.removeEventListener("paste", onPaste);
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-between w-full bg-[radial-gradient(circle_at_1px_1px,_#8e8e8e8e_1px,_transparent_0)] [background-size:20px_20px] bg-repeat">
|
||||
<Header />
|
||||
<div className="flex gap-3 absolute top-[68px] left-[24px] items-center z-10">
|
||||
<Plugins />
|
||||
<ImageSize />
|
||||
</div>
|
||||
{file ? <Editor file={file} /> : <></>}
|
||||
|
||||
{!file ? (
|
||||
<FileSelect
|
||||
onSelection={async (f) => {
|
||||
setFile(f);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default UploadDraw;
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,71 @@
|
|||
import { useState } from "react";
|
||||
import useResolution from "../hooks/useResolution";
|
||||
|
||||
type FileSelectProps = {
|
||||
onSelection: (file: File) => void;
|
||||
};
|
||||
|
||||
export default function FileSelect(props: FileSelectProps) {
|
||||
const { onSelection } = props;
|
||||
|
||||
const [uploadElemId] = useState(`file-upload-${Math.random().toString()}`);
|
||||
|
||||
const resolution = useResolution();
|
||||
|
||||
function onFileSelected(file: File) {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
// Skip non-image files
|
||||
const isImage = file.type.match("image.*");
|
||||
if (!isImage) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Check if file is larger than 20mb
|
||||
if (file.size > 20 * 1024 * 1024) {
|
||||
throw new Error("file too large");
|
||||
}
|
||||
onSelection(file);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line
|
||||
alert(`error: ${(e as any).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute flex w-screen h-screen justify-center items-center pointer-events-none">
|
||||
<label
|
||||
htmlFor={uploadElemId}
|
||||
className="grid border-[2px] border-[dashed] bg-[var(--mantine-color-body)] rounded-lg min-w-[600px] hover:bg-[#1971c2] hover:text-[#1971c2]-foreground pointer-events-auto"
|
||||
>
|
||||
<div
|
||||
className="grid p-16 w-full h-full"
|
||||
onDragOver={(ev) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}}
|
||||
>
|
||||
<input
|
||||
className="hidden"
|
||||
id={uploadElemId}
|
||||
name={uploadElemId}
|
||||
type="file"
|
||||
onChange={(ev) => {
|
||||
const file = ev.currentTarget.files?.[0];
|
||||
if (file) {
|
||||
onFileSelected(file);
|
||||
}
|
||||
}}
|
||||
accept="image/png, image/jpeg"
|
||||
/>
|
||||
<p className="text-center">
|
||||
{resolution === "desktop"
|
||||
? "Click here or drag an image file"
|
||||
: "Tap here to load your picture"}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import { RotateCw, Image } from "lucide-react";
|
||||
import { ActionIcon, Tooltip } from "@mantine/core";
|
||||
import { useStore } from "../lib/states";
|
||||
|
||||
const Header = () => {
|
||||
const [
|
||||
file,
|
||||
isInpainting,
|
||||
model,
|
||||
setFile,
|
||||
runInpainting,
|
||||
showPrevMask,
|
||||
hidePrevMask,
|
||||
] = useStore((state) => [
|
||||
state.file,
|
||||
state.isInpainting,
|
||||
state.settings.model,
|
||||
state.setFile,
|
||||
state.runInpainting,
|
||||
state.showPrevMask,
|
||||
state.hidePrevMask,
|
||||
]);
|
||||
|
||||
const handleRerunLastMask = () => {
|
||||
runInpainting();
|
||||
};
|
||||
|
||||
const onRerunMouseEnter = () => {
|
||||
showPrevMask();
|
||||
};
|
||||
|
||||
const onRerunMouseLeave = () => {
|
||||
hidePrevMask();
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="h-[60px] px-6 py-4 absolute top-[0] flex justify-between items-center w-full z-20 border-b backdrop-filter backdrop-blur-md">
|
||||
<div className="flex items-center gap-1">
|
||||
<Tooltip label="Upload image">
|
||||
<ActionIcon
|
||||
size={36}
|
||||
variant="default"
|
||||
disabled={isInpainting}
|
||||
component="label"
|
||||
>
|
||||
<Image />
|
||||
|
||||
<input
|
||||
className="hidden"
|
||||
type="file"
|
||||
onChange={(ev) => {
|
||||
const newFile = ev.currentTarget.files?.[0];
|
||||
if (newFile) {
|
||||
setFile(newFile);
|
||||
}
|
||||
}}
|
||||
accept="image/png, image/jpeg"
|
||||
/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
{file && !model.need_prompt ? (
|
||||
<Tooltip label="Rerun previous mask">
|
||||
<ActionIcon
|
||||
size={36}
|
||||
variant="default"
|
||||
disabled={isInpainting}
|
||||
onClick={handleRerunLastMask}
|
||||
onMouseEnter={onRerunMouseEnter}
|
||||
onMouseLeave={onRerunMouseLeave}
|
||||
>
|
||||
<div className="icon-button-icon-wrapper">
|
||||
<RotateCw />
|
||||
</div>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1"></div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { useStore } from "../lib/states";
|
||||
|
||||
const ImageSize = () => {
|
||||
const [imageWidth, imageHeight] = useStore((state) => [
|
||||
state.imageWidth,
|
||||
state.imageHeight,
|
||||
]);
|
||||
|
||||
if (!imageWidth || !imageHeight) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border bg-[var(--mantine-color-body)] rounded-lg px-2 py-[6px] z-10">
|
||||
{imageWidth}x{imageHeight}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageSize;
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
import {
|
||||
Blocks,
|
||||
Fullscreen,
|
||||
MousePointerClick,
|
||||
Slice,
|
||||
Smile,
|
||||
} from "lucide-react";
|
||||
import { useStore } from "../lib/states";
|
||||
import { PluginInfo } from "../lib/types";
|
||||
import { ActionIcon, Menu } from "@mantine/core";
|
||||
|
||||
export enum PluginName {
|
||||
RemoveBG = "RemoveBG",
|
||||
AnimeSeg = "AnimeSeg",
|
||||
RealESRGAN = "RealESRGAN",
|
||||
GFPGAN = "GFPGAN",
|
||||
RestoreFormer = "RestoreFormer",
|
||||
InteractiveSeg = "InteractiveSeg",
|
||||
}
|
||||
|
||||
// TODO: get plugin config from server and using form-render??
|
||||
const pluginMap = {
|
||||
[PluginName.RemoveBG]: {
|
||||
IconClass: Slice,
|
||||
showName: "RemoveBG",
|
||||
},
|
||||
[PluginName.AnimeSeg]: {
|
||||
IconClass: Slice,
|
||||
showName: "Anime Segmentation",
|
||||
},
|
||||
[PluginName.RealESRGAN]: {
|
||||
IconClass: Fullscreen,
|
||||
showName: "RealESRGAN",
|
||||
},
|
||||
[PluginName.GFPGAN]: {
|
||||
IconClass: Smile,
|
||||
showName: "GFPGAN",
|
||||
},
|
||||
[PluginName.RestoreFormer]: {
|
||||
IconClass: Smile,
|
||||
showName: "RestoreFormer",
|
||||
},
|
||||
[PluginName.InteractiveSeg]: {
|
||||
IconClass: MousePointerClick,
|
||||
showName: "Interactive Segmentation",
|
||||
},
|
||||
};
|
||||
|
||||
const Plugins = () => {
|
||||
const [
|
||||
file,
|
||||
plugins,
|
||||
isPluginRunning,
|
||||
updateInteractiveSegState,
|
||||
runRenderablePlugin,
|
||||
] = useStore((state) => [
|
||||
state.file,
|
||||
state.serverConfig.plugins,
|
||||
state.isPluginRunning,
|
||||
state.updateInteractiveSegState,
|
||||
state.runRenderablePlugin,
|
||||
]);
|
||||
const disabled = !file;
|
||||
|
||||
if (plugins.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onPluginClick = (genMask: boolean, pluginName: string) => {
|
||||
if (pluginName === PluginName.InteractiveSeg) {
|
||||
updateInteractiveSegState({ isInteractiveSeg: true });
|
||||
} else {
|
||||
runRenderablePlugin(genMask, pluginName);
|
||||
}
|
||||
};
|
||||
|
||||
const renderRealESRGANPlugin = () => {
|
||||
return (
|
||||
<Menu.Sub key="RealESRGAN">
|
||||
<Menu.Sub.Target>
|
||||
<Menu.Sub.Item disabled={disabled}>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Fullscreen />
|
||||
RealESRGAN
|
||||
</div>
|
||||
</Menu.Sub.Item>
|
||||
</Menu.Sub.Target>
|
||||
<Menu.Sub.Dropdown>
|
||||
<Menu.Item
|
||||
onClick={() =>
|
||||
runRenderablePlugin(false, PluginName.RealESRGAN, {
|
||||
upscale: 2,
|
||||
})
|
||||
}
|
||||
>
|
||||
upscale 2x
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={() =>
|
||||
runRenderablePlugin(false, PluginName.RealESRGAN, {
|
||||
upscale: 4,
|
||||
})
|
||||
}
|
||||
>
|
||||
upscale 4x
|
||||
</Menu.Item>
|
||||
</Menu.Sub.Dropdown>
|
||||
</Menu.Sub>
|
||||
);
|
||||
};
|
||||
|
||||
const renderGenImageAndMaskPlugin = (plugin: PluginInfo) => {
|
||||
const { IconClass, showName } = pluginMap[plugin.name as PluginName];
|
||||
return (
|
||||
<Menu.Sub key={plugin.name}>
|
||||
<Menu.Sub.Target>
|
||||
<Menu.Sub.Item>
|
||||
<div className="flex gap-2 items-center">
|
||||
<IconClass className="p-1" />
|
||||
{showName}
|
||||
</div>
|
||||
</Menu.Sub.Item>
|
||||
</Menu.Sub.Target>
|
||||
<Menu.Sub.Dropdown>
|
||||
<Menu.Item
|
||||
onClick={() => onPluginClick(false, plugin.name)}
|
||||
>
|
||||
Remove Background
|
||||
</Menu.Item>
|
||||
<Menu.Item onClick={() => onPluginClick(true, plugin.name)}>
|
||||
Generate Mask
|
||||
</Menu.Item>
|
||||
</Menu.Sub.Dropdown>
|
||||
</Menu.Sub>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPlugins = () => {
|
||||
return plugins.map((plugin: PluginInfo) => {
|
||||
const { IconClass, showName } =
|
||||
pluginMap[plugin.name as PluginName];
|
||||
if (plugin.name === PluginName.RealESRGAN) {
|
||||
return renderRealESRGANPlugin();
|
||||
}
|
||||
if (
|
||||
plugin.name === PluginName.RemoveBG ||
|
||||
plugin.name === PluginName.AnimeSeg
|
||||
) {
|
||||
return renderGenImageAndMaskPlugin(plugin);
|
||||
}
|
||||
return (
|
||||
<Menu.Item
|
||||
key={plugin.name}
|
||||
onClick={() => onPluginClick(false, plugin.name)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className="flex gap-2 items-center">
|
||||
<IconClass className="p-1" />
|
||||
{showName}
|
||||
</div>
|
||||
</Menu.Item>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<Menu.Target>
|
||||
<ActionIcon size={40} variant="default">
|
||||
{isPluginRunning ? (
|
||||
<div role="status">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="w-5 h-5 animate-spin fill-primary"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
<Blocks strokeWidth={1} />
|
||||
)}
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
|
||||
<Menu.Dropdown>{renderPlugins()}</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default Plugins;
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { useStore } from "../lib/states";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
|
||||
const useHotKey = (keys: string, callback: any, deps?: any[]) => {
|
||||
const disableShortCuts = useStore((state) => state.disableShortCuts);
|
||||
|
||||
const ref = useHotkeys(
|
||||
keys,
|
||||
callback,
|
||||
{ enabled: !disableShortCuts },
|
||||
deps,
|
||||
);
|
||||
return ref;
|
||||
};
|
||||
|
||||
export default useHotKey;
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { useEffect, useState } from "react"
|
||||
|
||||
function useImage(file: File | null): [HTMLImageElement, boolean] {
|
||||
const [image] = useState(new Image())
|
||||
const [isLoaded, setIsLoaded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!file) {
|
||||
return
|
||||
}
|
||||
image.onload = () => {
|
||||
setIsLoaded(true)
|
||||
}
|
||||
setIsLoaded(false)
|
||||
image.src = URL.createObjectURL(file)
|
||||
return () => {
|
||||
image.onload = null
|
||||
}
|
||||
}, [file, image])
|
||||
|
||||
return [image, isLoaded]
|
||||
}
|
||||
|
||||
export { useImage }
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import { API_ENDPOINT } from "../lib/api";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
export default function useInputImage() {
|
||||
const [inputImage, setInputImage] = useState<File | null>(null);
|
||||
|
||||
const fetchInputImage = useCallback(() => {
|
||||
const headers = new Headers();
|
||||
headers.append("pragma", "no-cache");
|
||||
headers.append("cache-control", "no-cache");
|
||||
|
||||
fetch(`${API_ENDPOINT}/inputimage`, { headers })
|
||||
.then(async (res) => {
|
||||
if (!res.ok) {
|
||||
return;
|
||||
}
|
||||
const filename = res.headers
|
||||
.get("content-disposition")
|
||||
?.split("filename=")[1]
|
||||
.split(";")[0];
|
||||
|
||||
const data = await res.blob();
|
||||
if (data && data.type.startsWith("image")) {
|
||||
const userInput = new File(
|
||||
[data],
|
||||
filename !== undefined ? filename : "inputImage",
|
||||
);
|
||||
setInputImage(userInput);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
}, [setInputImage]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchInputImage();
|
||||
}, [fetchInputImage]);
|
||||
|
||||
return inputImage;
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
const useResolution = () => {
|
||||
const [width, setWidth] = useState(window.innerWidth)
|
||||
|
||||
const windowSizeHandler = useCallback(() => {
|
||||
setWidth(window.innerWidth)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', windowSizeHandler)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', windowSizeHandler)
|
||||
}
|
||||
})
|
||||
|
||||
if (width < 768) {
|
||||
return 'mobile'
|
||||
}
|
||||
|
||||
if (width >= 768 && width < 1224) {
|
||||
return 'tablet'
|
||||
}
|
||||
|
||||
if (width >= 1224) {
|
||||
return 'desktop'
|
||||
}
|
||||
}
|
||||
|
||||
export default useResolution
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as UploadDraw } from "./UploadDraw";
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
import {
|
||||
Filename,
|
||||
ModelInfo,
|
||||
PowerPaintTask,
|
||||
Rect,
|
||||
ServerConfig,
|
||||
} from "./types";
|
||||
import { Settings } from "./states";
|
||||
import { convertToBase64, srcToFile } from "./utils";
|
||||
import axios from "axios";
|
||||
|
||||
export const API_ENDPOINT = import.meta.env.DEV
|
||||
? import.meta.env.VITE_BACKEND + "/api/v1"
|
||||
: "/api/v1";
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: API_ENDPOINT,
|
||||
});
|
||||
|
||||
export default async function inpaint(
|
||||
imageFile: File,
|
||||
settings: Settings,
|
||||
croperRect: Rect,
|
||||
extenderState: Rect,
|
||||
mask: File | Blob,
|
||||
paintByExampleImage: File | null = null,
|
||||
) {
|
||||
const imageBase64 = await convertToBase64(imageFile);
|
||||
const maskBase64 = await convertToBase64(mask);
|
||||
const exampleImageBase64 = paintByExampleImage
|
||||
? await convertToBase64(paintByExampleImage)
|
||||
: null;
|
||||
|
||||
const res = await fetch(`${API_ENDPOINT}/inpaint`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
image: imageBase64,
|
||||
mask: maskBase64,
|
||||
ldm_steps: settings.ldmSteps,
|
||||
ldm_sampler: settings.ldmSampler,
|
||||
zits_wireframe: settings.zitsWireframe,
|
||||
cv2_flag: settings.cv2Flag,
|
||||
cv2_radius: settings.cv2Radius,
|
||||
hd_strategy: "Crop",
|
||||
hd_strategy_crop_triger_size: 640,
|
||||
hd_strategy_crop_margin: 128,
|
||||
hd_trategy_resize_imit: 2048,
|
||||
prompt: settings.prompt,
|
||||
negative_prompt: settings.negativePrompt,
|
||||
use_croper: settings.showCropper,
|
||||
croper_x: croperRect.x,
|
||||
croper_y: croperRect.y,
|
||||
croper_height: croperRect.height,
|
||||
croper_width: croperRect.width,
|
||||
use_extender: settings.showExtender,
|
||||
extender_x: extenderState.x,
|
||||
extender_y: extenderState.y,
|
||||
extender_height: extenderState.height,
|
||||
extender_width: extenderState.width,
|
||||
sd_mask_blur: settings.sdMaskBlur,
|
||||
sd_strength: settings.sdStrength,
|
||||
sd_steps: settings.sdSteps,
|
||||
sd_guidance_scale: settings.sdGuidanceScale,
|
||||
sd_sampler: settings.sdSampler,
|
||||
sd_seed: settings.seedFixed ? settings.seed : -1,
|
||||
sd_match_histograms: settings.sdMatchHistograms,
|
||||
sd_freeu: settings.enableFreeu,
|
||||
sd_freeu_config: settings.freeuConfig,
|
||||
sd_lcm_lora: settings.enableLCMLora,
|
||||
paint_by_example_example_image: exampleImageBase64,
|
||||
p2p_image_guidance_scale: settings.p2pImageGuidanceScale,
|
||||
enable_controlnet: settings.enableControlnet,
|
||||
controlnet_conditioning_scale: settings.controlnetConditioningScale,
|
||||
controlnet_method: settings.controlnetMethod
|
||||
? settings.controlnetMethod
|
||||
: "",
|
||||
powerpaint_task: settings.showExtender
|
||||
? PowerPaintTask.outpainting
|
||||
: settings.powerpaintTask,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const blob = await res.blob();
|
||||
return {
|
||||
blob: URL.createObjectURL(blob),
|
||||
seed: res.headers.get("X-Seed"),
|
||||
};
|
||||
}
|
||||
const errors = await res.json();
|
||||
throw new Error(`Something went wrong: ${errors.errors}`);
|
||||
}
|
||||
|
||||
export async function getServerConfig(): Promise<ServerConfig> {
|
||||
const res = await api.get(`/server-config`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function switchModel(name: string): Promise<ModelInfo> {
|
||||
const res = await api.post(`/model`, { name });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function switchPluginModel(
|
||||
plugin_name: string,
|
||||
model_name: string,
|
||||
) {
|
||||
return api.post(`/switch_plugin_model`, { plugin_name, model_name });
|
||||
}
|
||||
|
||||
export async function currentModel(): Promise<ModelInfo> {
|
||||
const res = await api.get("/model");
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function runPlugin(
|
||||
genMask: boolean,
|
||||
name: string,
|
||||
imageFile: File,
|
||||
upscale?: number,
|
||||
clicks?: number[][],
|
||||
) {
|
||||
const imageBase64 = await convertToBase64(imageFile);
|
||||
const p = genMask ? "run_plugin_gen_mask" : "run_plugin_gen_image";
|
||||
const res = await fetch(`${API_ENDPOINT}/${p}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
image: imageBase64,
|
||||
upscale,
|
||||
clicks,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const blob = await res.blob();
|
||||
return { blob: URL.createObjectURL(blob) };
|
||||
}
|
||||
const errMsg = await res.json();
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
|
||||
export async function getMediaFile(tab: string, filename: string) {
|
||||
const res = await fetch(
|
||||
`${API_ENDPOINT}/media_file?tab=${tab}&filename=${encodeURIComponent(
|
||||
filename,
|
||||
)}`,
|
||||
{
|
||||
method: "GET",
|
||||
},
|
||||
);
|
||||
if (res.ok) {
|
||||
const blob = await res.blob();
|
||||
const file = new File([blob], filename, {
|
||||
type: res.headers.get("Content-Type") ?? "image/png",
|
||||
});
|
||||
return file;
|
||||
}
|
||||
const errMsg = await res.json();
|
||||
throw new Error(errMsg.errors);
|
||||
}
|
||||
|
||||
export async function getMedias(tab: string): Promise<Filename[]> {
|
||||
const res = await api.get(`medias`, { params: { tab } });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function downloadToOutput(
|
||||
image: HTMLImageElement,
|
||||
filename: string,
|
||||
mimeType: string,
|
||||
) {
|
||||
const file = await srcToFile(image.src, filename, mimeType);
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_ENDPOINT}/save_image`, {
|
||||
method: "POST",
|
||||
body: fd,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const errMsg = await res.text();
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Something went wrong: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSamplers(): Promise<string[]> {
|
||||
const res = await api.post("/samplers");
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function postAdjustMask(
|
||||
mask: File | Blob,
|
||||
operate: "expand" | "shrink" | "reverse",
|
||||
kernel_size: number,
|
||||
) {
|
||||
const maskBase64 = await convertToBase64(mask);
|
||||
const res = await fetch(`${API_ENDPOINT}/adjust_mask`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
mask: maskBase64,
|
||||
operate: operate,
|
||||
kernel_size: kernel_size,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const blob = await res.blob();
|
||||
return blob;
|
||||
}
|
||||
const errMsg = await res.json();
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
|
||||
export async function submitMask(imageFile: File, mask: File | Blob) {
|
||||
const imageBase64 = await convertToBase64(imageFile);
|
||||
const maskBase64 = await convertToBase64(mask);
|
||||
|
||||
const res = await fetch(`${API_ENDPOINT}/submit-mask`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
image: imageBase64,
|
||||
mask: maskBase64,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const blob = await res.blob();
|
||||
return {
|
||||
blob: URL.createObjectURL(blob),
|
||||
seed: res.headers.get("X-Seed"),
|
||||
};
|
||||
}
|
||||
// const errors = await res.json();
|
||||
throw new Error(`Submit successfull.`);
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
export const ACCENT_COLOR = "#ffcc00bb"
|
||||
export const DEFAULT_BRUSH_SIZE = 40
|
||||
export const MIN_BRUSH_SIZE = 3
|
||||
export const MAX_BRUSH_SIZE = 200
|
||||
export const MODEL_TYPE_INPAINT = "inpaint"
|
||||
export const MODEL_TYPE_DIFFUSERS_SD = "diffusers_sd"
|
||||
export const MODEL_TYPE_DIFFUSERS_SDXL = "diffusers_sdxl"
|
||||
export const MODEL_TYPE_DIFFUSERS_SD_INPAINT = "diffusers_sd_inpaint"
|
||||
export const MODEL_TYPE_DIFFUSERS_SDXL_INPAINT = "diffusers_sdxl_inpaint"
|
||||
export const MODEL_TYPE_OTHER = "diffusers_other"
|
||||
export const BRUSH_COLOR = "#ffcc00bb"
|
||||
|
||||
export const LDM = "ldm"
|
||||
export const CV2 = "cv2"
|
||||
|
||||
export const PAINT_BY_EXAMPLE = "Fantasy-Studio/Paint-by-Example"
|
||||
export const INSTRUCT_PIX2PIX = "timbrooks/instruct-pix2pix"
|
||||
export const KANDINSKY_2_2 = "kandinsky-community/kandinsky-2-2-decoder-inpaint"
|
||||
export const POWERPAINT = "Sanster/PowerPaint-V1-stable-diffusion-inpainting"
|
||||
export const ANYTEXT = "Sanster/AnyText"
|
||||
|
||||
export const DEFAULT_NEGATIVE_PROMPT =
|
||||
"out of frame, lowres, error, cropped, worst quality, low quality, jpeg artifacts, ugly, duplicate, morbid, mutilated, out of frame, mutation, deformed, blurry, dehydrated, bad anatomy, bad proportions, extra limbs, disfigured, gross proportions, malformed limbs, watermark, signature"
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,136 @@
|
|||
export interface Filename {
|
||||
name: string
|
||||
height: number
|
||||
width: number
|
||||
ctime: number
|
||||
mtime: number
|
||||
}
|
||||
|
||||
export interface PluginInfo {
|
||||
name: string
|
||||
support_gen_image: boolean
|
||||
support_gen_mask: boolean
|
||||
}
|
||||
|
||||
export interface ServerConfig {
|
||||
plugins: PluginInfo[]
|
||||
modelInfos: ModelInfo[]
|
||||
removeBGModel: string
|
||||
removeBGModels: string[]
|
||||
realesrganModel: string
|
||||
realesrganModels: string[]
|
||||
interactiveSegModel: string
|
||||
interactiveSegModels: string[]
|
||||
enableFileManager: boolean
|
||||
enableAutoSaving: boolean
|
||||
enableControlnet: boolean
|
||||
controlnetMethod: string
|
||||
disableModelSwitch: boolean
|
||||
isDesktop: boolean
|
||||
samplers: string[]
|
||||
}
|
||||
|
||||
export interface GenInfo {
|
||||
prompt: string
|
||||
negative_prompt: string
|
||||
}
|
||||
|
||||
export interface ModelInfo {
|
||||
name: string
|
||||
path: string
|
||||
model_type:
|
||||
| "inpaint"
|
||||
| "diffusers_sd"
|
||||
| "diffusers_sdxl"
|
||||
| "diffusers_sd_inpaint"
|
||||
| "diffusers_sdxl_inpaint"
|
||||
| "diffusers_other"
|
||||
support_strength: boolean
|
||||
support_outpainting: boolean
|
||||
support_controlnet: boolean
|
||||
controlnets: string[]
|
||||
support_freeu: boolean
|
||||
support_lcm_lora: boolean
|
||||
need_prompt: boolean
|
||||
is_single_file_diffusers: boolean
|
||||
}
|
||||
|
||||
export enum PluginName {
|
||||
RemoveBG = "RemoveBG",
|
||||
AnimeSeg = "AnimeSeg",
|
||||
RealESRGAN = "RealESRGAN",
|
||||
GFPGAN = "GFPGAN",
|
||||
RestoreFormer = "RestoreFormer",
|
||||
InteractiveSeg = "InteractiveSeg",
|
||||
}
|
||||
|
||||
export interface PluginParams {
|
||||
upscale: number
|
||||
}
|
||||
|
||||
export enum SortBy {
|
||||
NAME = "name",
|
||||
CTIME = "ctime",
|
||||
MTIME = "mtime",
|
||||
}
|
||||
|
||||
export enum SortOrder {
|
||||
DESCENDING = "desc",
|
||||
ASCENDING = "asc",
|
||||
}
|
||||
|
||||
export enum LDMSampler {
|
||||
ddim = "ddim",
|
||||
plms = "plms",
|
||||
}
|
||||
|
||||
export enum CV2Flag {
|
||||
INPAINT_NS = "INPAINT_NS",
|
||||
INPAINT_TELEA = "INPAINT_TELEA",
|
||||
}
|
||||
|
||||
export interface Rect {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface FreeuConfig {
|
||||
s1: number
|
||||
s2: number
|
||||
b1: number
|
||||
b2: number
|
||||
}
|
||||
|
||||
export interface Point {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export interface Line {
|
||||
size?: number
|
||||
pts: Point[]
|
||||
}
|
||||
|
||||
export type LineGroup = Array<Line>
|
||||
|
||||
export interface Size {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export enum ExtenderDirection {
|
||||
x = "x",
|
||||
y = "y",
|
||||
xy = "xy",
|
||||
}
|
||||
|
||||
export enum PowerPaintTask {
|
||||
text_guided = "text-guided",
|
||||
shape_guided = "shape-guided",
|
||||
object_remove = "object-remove",
|
||||
outpainting = "outpainting",
|
||||
}
|
||||
|
||||
export type AdjustMaskOperate = "expand" | "shrink" | "reverse"
|
||||
|
|
@ -0,0 +1,246 @@
|
|||
import { type ClassValue, clsx } from "clsx";
|
||||
import { SyntheticEvent } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { LineGroup } from "./types";
|
||||
import { BRUSH_COLOR } from "./const";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function keepGUIAlive() {
|
||||
async function getRequest(url = "") {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
cache: "no-cache",
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
const keepAliveServer = () => {
|
||||
const url = document.location;
|
||||
const route = "/flaskwebgui-keep-server-alive";
|
||||
getRequest(url + route).then((data) => {
|
||||
return data;
|
||||
});
|
||||
};
|
||||
|
||||
const intervalRequest = 3 * 1000;
|
||||
keepAliveServer();
|
||||
setInterval(keepAliveServer, intervalRequest);
|
||||
}
|
||||
|
||||
export function dataURItoBlob(dataURI: string) {
|
||||
const mime = dataURI.split(",")[0].split(":")[1].split(";")[0];
|
||||
const binary = atob(dataURI.split(",")[1]);
|
||||
const array = [];
|
||||
for (let i = 0; i < binary.length; i += 1) {
|
||||
array.push(binary.charCodeAt(i));
|
||||
}
|
||||
return new Blob([new Uint8Array(array)], { type: mime });
|
||||
}
|
||||
|
||||
export function loadImage(image: HTMLImageElement, src: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const initSRC = image.src;
|
||||
const img = image;
|
||||
img.onload = resolve;
|
||||
img.onerror = (err) => {
|
||||
img.src = initSRC;
|
||||
reject(err);
|
||||
};
|
||||
img.src = src;
|
||||
});
|
||||
}
|
||||
|
||||
export async function blobToImage(blob: Blob) {
|
||||
const dataURL = URL.createObjectURL(blob);
|
||||
const newImage = new Image();
|
||||
await loadImage(newImage, dataURL);
|
||||
return newImage;
|
||||
}
|
||||
|
||||
export function canvasToImage(
|
||||
canvas: HTMLCanvasElement,
|
||||
): Promise<HTMLImageElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
|
||||
image.addEventListener("load", () => {
|
||||
resolve(image);
|
||||
});
|
||||
|
||||
image.addEventListener("error", (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
image.src = canvas.toDataURL();
|
||||
});
|
||||
}
|
||||
|
||||
export function fileToImage(file: File): Promise<HTMLImageElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const image = new Image();
|
||||
image.onload = () => {
|
||||
resolve(image);
|
||||
};
|
||||
image.onerror = () => {
|
||||
reject("无法加载图像。");
|
||||
};
|
||||
image.src = reader.result as string;
|
||||
};
|
||||
reader.onerror = () => {
|
||||
reject("无法读取文件。");
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
export function srcToFile(src: string, fileName: string, mimeType: string) {
|
||||
return fetch(src)
|
||||
.then(function (res) {
|
||||
return res.arrayBuffer();
|
||||
})
|
||||
.then(function (buf) {
|
||||
return new File([buf], fileName, { type: mimeType });
|
||||
});
|
||||
}
|
||||
|
||||
export async function askWritePermission() {
|
||||
try {
|
||||
// The clipboard-write permission is granted automatically to pages
|
||||
// when they are the active tab. So it's not required, but it's more safe.
|
||||
const { state } = await navigator.permissions.query({
|
||||
name: "clipboard-write" as PermissionName,
|
||||
});
|
||||
return state === "granted";
|
||||
} catch (error) {
|
||||
// Browser compatibility / Security error (ONLY HTTPS) ...
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function canvasToBlob(canvas: HTMLCanvasElement, mime: string): Promise<any> {
|
||||
return new Promise((resolve, reject) =>
|
||||
canvas.toBlob(async (d) => {
|
||||
if (d) {
|
||||
resolve(d);
|
||||
} else {
|
||||
reject(new Error("Expected toBlob() to be defined"));
|
||||
}
|
||||
}, mime),
|
||||
);
|
||||
}
|
||||
|
||||
const setToClipboard = async (blob: any) => {
|
||||
const data = [new ClipboardItem({ [blob.type]: blob })];
|
||||
await navigator.clipboard.write(data);
|
||||
};
|
||||
|
||||
export function isRightClick(ev: SyntheticEvent) {
|
||||
const mouseEvent = ev.nativeEvent as MouseEvent;
|
||||
return mouseEvent.button === 2;
|
||||
}
|
||||
|
||||
export function isMidClick(ev: SyntheticEvent) {
|
||||
const mouseEvent = ev.nativeEvent as MouseEvent;
|
||||
return mouseEvent.button === 1;
|
||||
}
|
||||
|
||||
export async function copyCanvasImage(canvas: HTMLCanvasElement) {
|
||||
const blob = await canvasToBlob(canvas, "image/png");
|
||||
try {
|
||||
await setToClipboard(blob);
|
||||
} catch {
|
||||
console.log("Copy image failed!");
|
||||
}
|
||||
}
|
||||
|
||||
export function downloadImage(uri: string, name: string) {
|
||||
const link = document.createElement("a");
|
||||
link.href = uri;
|
||||
link.download = name;
|
||||
|
||||
// this is necessary as link.click() does not work on the latest firefox
|
||||
link.dispatchEvent(
|
||||
new MouseEvent("click", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: window,
|
||||
}),
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
// For Firefox it is necessary to delay revoking the ObjectURL
|
||||
// window.URL.revokeObjectURL(base64)
|
||||
link.remove();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
export function mouseXY(ev: SyntheticEvent) {
|
||||
const mouseEvent = ev.nativeEvent as MouseEvent;
|
||||
return { x: mouseEvent.offsetX, y: mouseEvent.offsetY };
|
||||
}
|
||||
|
||||
export function drawLines(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
lines: LineGroup,
|
||||
color = BRUSH_COLOR,
|
||||
) {
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineCap = "round";
|
||||
ctx.lineJoin = "round";
|
||||
|
||||
lines.forEach((line) => {
|
||||
if (!line?.pts.length || !line.size) {
|
||||
return;
|
||||
}
|
||||
ctx.lineWidth = line.size;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(line.pts[0].x, line.pts[0].y);
|
||||
line.pts.forEach((pt) => ctx.lineTo(pt.x, pt.y));
|
||||
ctx.stroke();
|
||||
});
|
||||
}
|
||||
|
||||
export const generateMask = (
|
||||
imageWidth: number,
|
||||
imageHeight: number,
|
||||
lineGroups: LineGroup[],
|
||||
maskImages: HTMLImageElement[] = [],
|
||||
lineGroupsColor: string = "white",
|
||||
): HTMLCanvasElement => {
|
||||
const maskCanvas = document.createElement("canvas");
|
||||
maskCanvas.width = imageWidth;
|
||||
maskCanvas.height = imageHeight;
|
||||
const ctx = maskCanvas.getContext("2d");
|
||||
if (!ctx) {
|
||||
throw new Error("could not retrieve mask canvas");
|
||||
}
|
||||
|
||||
maskImages.forEach((maskImage) => {
|
||||
ctx.drawImage(maskImage, 0, 0, imageWidth, imageHeight);
|
||||
});
|
||||
|
||||
lineGroups.forEach((lineGroup) => {
|
||||
drawLines(ctx, lineGroup, lineGroupsColor);
|
||||
});
|
||||
|
||||
return maskCanvas;
|
||||
};
|
||||
|
||||
export const convertToBase64 = (fileOrBlob: File | Blob): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const base64String = event.target?.result as string;
|
||||
resolve(base64String);
|
||||
};
|
||||
reader.onerror = (error) => {
|
||||
reject(error);
|
||||
};
|
||||
reader.readAsDataURL(fileOrBlob);
|
||||
});
|
||||
};
|
||||
|
|
@ -1,231 +1,256 @@
|
|||
import {
|
||||
Filename,
|
||||
GenInfo,
|
||||
ModelInfo,
|
||||
PowerPaintTask,
|
||||
Rect,
|
||||
ServerConfig,
|
||||
Filename,
|
||||
GenInfo,
|
||||
ModelInfo,
|
||||
PowerPaintTask,
|
||||
Rect,
|
||||
ServerConfig,
|
||||
} from "@/lib/types";
|
||||
import { Settings } from "@/lib/states";
|
||||
import { convertToBase64, srcToFile } from "@/lib/utils";
|
||||
import axios from "axios";
|
||||
|
||||
export const API_ENDPOINT = import.meta.env.DEV
|
||||
? import.meta.env.VITE_BACKEND + "/api/v1"
|
||||
: "/api/v1";
|
||||
? import.meta.env.VITE_BACKEND + "/api/v1"
|
||||
: "/api/v1";
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: API_ENDPOINT,
|
||||
baseURL: API_ENDPOINT,
|
||||
});
|
||||
|
||||
export default async function inpaint(
|
||||
imageFile: File,
|
||||
settings: Settings,
|
||||
croperRect: Rect,
|
||||
extenderState: Rect,
|
||||
mask: File | Blob,
|
||||
paintByExampleImage: File | null = null
|
||||
imageFile: File,
|
||||
settings: Settings,
|
||||
croperRect: Rect,
|
||||
extenderState: Rect,
|
||||
mask: File | Blob,
|
||||
paintByExampleImage: File | null = null,
|
||||
) {
|
||||
const imageBase64 = await convertToBase64(imageFile);
|
||||
const maskBase64 = await convertToBase64(mask);
|
||||
const exampleImageBase64 = paintByExampleImage
|
||||
? await convertToBase64(paintByExampleImage)
|
||||
: null;
|
||||
const imageBase64 = await convertToBase64(imageFile);
|
||||
const maskBase64 = await convertToBase64(mask);
|
||||
const exampleImageBase64 = paintByExampleImage
|
||||
? await convertToBase64(paintByExampleImage)
|
||||
: null;
|
||||
|
||||
const res = await fetch(`${API_ENDPOINT}/inpaint`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
image: imageBase64,
|
||||
mask: maskBase64,
|
||||
ldm_steps: settings.ldmSteps,
|
||||
ldm_sampler: settings.ldmSampler,
|
||||
zits_wireframe: settings.zitsWireframe,
|
||||
cv2_flag: settings.cv2Flag,
|
||||
cv2_radius: settings.cv2Radius,
|
||||
hd_strategy: "Crop",
|
||||
hd_strategy_crop_triger_size: 640,
|
||||
hd_strategy_crop_margin: 128,
|
||||
hd_trategy_resize_imit: 2048,
|
||||
prompt: settings.prompt,
|
||||
negative_prompt: settings.negativePrompt,
|
||||
use_croper: settings.showCropper,
|
||||
croper_x: croperRect.x,
|
||||
croper_y: croperRect.y,
|
||||
croper_height: croperRect.height,
|
||||
croper_width: croperRect.width,
|
||||
use_extender: settings.showExtender,
|
||||
extender_x: extenderState.x,
|
||||
extender_y: extenderState.y,
|
||||
extender_height: extenderState.height,
|
||||
extender_width: extenderState.width,
|
||||
sd_mask_blur: settings.sdMaskBlur,
|
||||
sd_strength: settings.sdStrength,
|
||||
sd_steps: settings.sdSteps,
|
||||
sd_guidance_scale: settings.sdGuidanceScale,
|
||||
sd_sampler: settings.sdSampler,
|
||||
sd_seed: settings.seedFixed ? settings.seed : -1,
|
||||
sd_match_histograms: settings.sdMatchHistograms,
|
||||
sd_freeu: settings.enableFreeu,
|
||||
sd_freeu_config: settings.freeuConfig,
|
||||
sd_lcm_lora: settings.enableLCMLora,
|
||||
paint_by_example_example_image: exampleImageBase64,
|
||||
p2p_image_guidance_scale: settings.p2pImageGuidanceScale,
|
||||
enable_controlnet: settings.enableControlnet,
|
||||
controlnet_conditioning_scale: settings.controlnetConditioningScale,
|
||||
controlnet_method: settings.controlnetMethod
|
||||
? settings.controlnetMethod
|
||||
: "",
|
||||
powerpaint_task: settings.showExtender
|
||||
? PowerPaintTask.outpainting
|
||||
: settings.powerpaintTask,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const blob = await res.blob();
|
||||
return {
|
||||
blob: URL.createObjectURL(blob),
|
||||
seed: res.headers.get("X-Seed"),
|
||||
};
|
||||
}
|
||||
const errors = await res.json();
|
||||
throw new Error(`Something went wrong: ${errors.errors}`);
|
||||
const res = await fetch(`${API_ENDPOINT}/inpaint`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
image: imageBase64,
|
||||
mask: maskBase64,
|
||||
ldm_steps: settings.ldmSteps,
|
||||
ldm_sampler: settings.ldmSampler,
|
||||
zits_wireframe: settings.zitsWireframe,
|
||||
cv2_flag: settings.cv2Flag,
|
||||
cv2_radius: settings.cv2Radius,
|
||||
hd_strategy: "Crop",
|
||||
hd_strategy_crop_triger_size: 640,
|
||||
hd_strategy_crop_margin: 128,
|
||||
hd_trategy_resize_imit: 2048,
|
||||
prompt: settings.prompt,
|
||||
negative_prompt: settings.negativePrompt,
|
||||
use_croper: settings.showCropper,
|
||||
croper_x: croperRect.x,
|
||||
croper_y: croperRect.y,
|
||||
croper_height: croperRect.height,
|
||||
croper_width: croperRect.width,
|
||||
use_extender: settings.showExtender,
|
||||
extender_x: extenderState.x,
|
||||
extender_y: extenderState.y,
|
||||
extender_height: extenderState.height,
|
||||
extender_width: extenderState.width,
|
||||
sd_mask_blur: settings.sdMaskBlur,
|
||||
sd_strength: settings.sdStrength,
|
||||
sd_steps: settings.sdSteps,
|
||||
sd_guidance_scale: settings.sdGuidanceScale,
|
||||
sd_sampler: settings.sdSampler,
|
||||
sd_seed: settings.seedFixed ? settings.seed : -1,
|
||||
sd_match_histograms: settings.sdMatchHistograms,
|
||||
sd_freeu: settings.enableFreeu,
|
||||
sd_freeu_config: settings.freeuConfig,
|
||||
sd_lcm_lora: settings.enableLCMLora,
|
||||
paint_by_example_example_image: exampleImageBase64,
|
||||
p2p_image_guidance_scale: settings.p2pImageGuidanceScale,
|
||||
enable_controlnet: settings.enableControlnet,
|
||||
controlnet_conditioning_scale: settings.controlnetConditioningScale,
|
||||
controlnet_method: settings.controlnetMethod
|
||||
? settings.controlnetMethod
|
||||
: "",
|
||||
powerpaint_task: settings.showExtender
|
||||
? PowerPaintTask.outpainting
|
||||
: settings.powerpaintTask,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const blob = await res.blob();
|
||||
return {
|
||||
blob: URL.createObjectURL(blob),
|
||||
seed: res.headers.get("X-Seed"),
|
||||
};
|
||||
}
|
||||
const errors = await res.json();
|
||||
throw new Error(`Something went wrong: ${errors.errors}`);
|
||||
}
|
||||
|
||||
export async function getServerConfig(): Promise<ServerConfig> {
|
||||
const res = await api.get(`/server-config`);
|
||||
return res.data;
|
||||
const res = await api.get(`/server-config`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function switchModel(name: string): Promise<ModelInfo> {
|
||||
const res = await api.post(`/model`, { name });
|
||||
return res.data;
|
||||
const res = await api.post(`/model`, { name });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function switchPluginModel(
|
||||
plugin_name: string,
|
||||
model_name: string
|
||||
plugin_name: string,
|
||||
model_name: string,
|
||||
) {
|
||||
return api.post(`/switch_plugin_model`, { plugin_name, model_name });
|
||||
return api.post(`/switch_plugin_model`, { plugin_name, model_name });
|
||||
}
|
||||
|
||||
export async function currentModel(): Promise<ModelInfo> {
|
||||
const res = await api.get("/model");
|
||||
return res.data;
|
||||
const res = await api.get("/model");
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function runPlugin(
|
||||
genMask: boolean,
|
||||
name: string,
|
||||
imageFile: File,
|
||||
upscale?: number,
|
||||
clicks?: number[][]
|
||||
genMask: boolean,
|
||||
name: string,
|
||||
imageFile: File,
|
||||
upscale?: number,
|
||||
clicks?: number[][],
|
||||
) {
|
||||
const imageBase64 = await convertToBase64(imageFile);
|
||||
const p = genMask ? "run_plugin_gen_mask" : "run_plugin_gen_image";
|
||||
const res = await fetch(`${API_ENDPOINT}/${p}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
image: imageBase64,
|
||||
upscale,
|
||||
clicks,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const blob = await res.blob();
|
||||
return { blob: URL.createObjectURL(blob) };
|
||||
}
|
||||
const errMsg = await res.json();
|
||||
throw new Error(errMsg);
|
||||
const imageBase64 = await convertToBase64(imageFile);
|
||||
const p = genMask ? "run_plugin_gen_mask" : "run_plugin_gen_image";
|
||||
const res = await fetch(`${API_ENDPOINT}/${p}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
image: imageBase64,
|
||||
upscale,
|
||||
clicks,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const blob = await res.blob();
|
||||
return { blob: URL.createObjectURL(blob) };
|
||||
}
|
||||
const errMsg = await res.json();
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
|
||||
export async function getMediaFile(tab: string, filename: string) {
|
||||
const res = await fetch(
|
||||
`${API_ENDPOINT}/media_file?tab=${tab}&filename=${encodeURIComponent(
|
||||
filename
|
||||
)}`,
|
||||
{
|
||||
method: "GET",
|
||||
}
|
||||
);
|
||||
if (res.ok) {
|
||||
const blob = await res.blob();
|
||||
const file = new File([blob], filename, {
|
||||
type: res.headers.get("Content-Type") ?? "image/png",
|
||||
});
|
||||
return file;
|
||||
}
|
||||
const errMsg = await res.json();
|
||||
throw new Error(errMsg.errors);
|
||||
const res = await fetch(
|
||||
`${API_ENDPOINT}/media_file?tab=${tab}&filename=${encodeURIComponent(
|
||||
filename,
|
||||
)}`,
|
||||
{
|
||||
method: "GET",
|
||||
},
|
||||
);
|
||||
if (res.ok) {
|
||||
const blob = await res.blob();
|
||||
const file = new File([blob], filename, {
|
||||
type: res.headers.get("Content-Type") ?? "image/png",
|
||||
});
|
||||
return file;
|
||||
}
|
||||
const errMsg = await res.json();
|
||||
throw new Error(errMsg.errors);
|
||||
}
|
||||
|
||||
export async function getMedias(tab: string): Promise<Filename[]> {
|
||||
const res = await api.get(`medias`, { params: { tab } });
|
||||
return res.data;
|
||||
const res = await api.get(`medias`, { params: { tab } });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function downloadToOutput(
|
||||
image: HTMLImageElement,
|
||||
filename: string,
|
||||
mimeType: string
|
||||
image: HTMLImageElement,
|
||||
filename: string,
|
||||
mimeType: string,
|
||||
) {
|
||||
const file = await srcToFile(image.src, filename, mimeType);
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
const file = await srcToFile(image.src, filename, mimeType);
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_ENDPOINT}/save_image`, {
|
||||
method: "POST",
|
||||
body: fd,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const errMsg = await res.text();
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Something went wrong: ${error}`);
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`${API_ENDPOINT}/save_image`, {
|
||||
method: "POST",
|
||||
body: fd,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const errMsg = await res.text();
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Something went wrong: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getGenInfo(file: File): Promise<GenInfo> {
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
const res = await api.post(`/gen-info`, fd);
|
||||
return res.data;
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
const res = await api.post(`/gen-info`, fd);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function getSamplers(): Promise<string[]> {
|
||||
const res = await api.post("/samplers");
|
||||
return res.data;
|
||||
const res = await api.post("/samplers");
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function postAdjustMask(
|
||||
mask: File | Blob,
|
||||
operate: "expand" | "shrink" | "reverse",
|
||||
kernel_size: number
|
||||
mask: File | Blob,
|
||||
operate: "expand" | "shrink" | "reverse",
|
||||
kernel_size: number,
|
||||
) {
|
||||
const maskBase64 = await convertToBase64(mask);
|
||||
const res = await fetch(`${API_ENDPOINT}/adjust_mask`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
mask: maskBase64,
|
||||
operate: operate,
|
||||
kernel_size: kernel_size,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const blob = await res.blob();
|
||||
return blob;
|
||||
}
|
||||
const errMsg = await res.json();
|
||||
throw new Error(errMsg);
|
||||
const maskBase64 = await convertToBase64(mask);
|
||||
const res = await fetch(`${API_ENDPOINT}/adjust_mask`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
mask: maskBase64,
|
||||
operate: operate,
|
||||
kernel_size: kernel_size,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const blob = await res.blob();
|
||||
return blob;
|
||||
}
|
||||
const errMsg = await res.json();
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
|
||||
export async function submitMask(imageFile: File, mask: File | Blob) {
|
||||
const imageBase64 = await convertToBase64(imageFile);
|
||||
const maskBase64 = await convertToBase64(mask);
|
||||
|
||||
const res = await fetch(`${API_ENDPOINT}/submit-mask`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
image: imageBase64,
|
||||
mask: maskBase64,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const blob = await res.blob();
|
||||
return {
|
||||
blob: URL.createObjectURL(blob),
|
||||
seed: res.headers.get("X-Seed"),
|
||||
};
|
||||
}
|
||||
// const errors = await res.json();
|
||||
throw new Error(`Submit successfull.`);
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,22 +1,18 @@
|
|||
import React from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
import "inter-ui/inter.css"
|
||||
import App from "./App.tsx"
|
||||
import "./globals.css"
|
||||
import { ThemeProvider } from "next-themes"
|
||||
import { TooltipProvider } from "./components/ui/tooltip.tsx"
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import "./globals.css";
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
import { MantineProvider } from "@mantine/core";
|
||||
import { Notifications } from "@mantine/notifications";
|
||||
import "@mantine/core/styles.css";
|
||||
import "@mantine/notifications/styles.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider defaultTheme="dark" disableTransitionOnChange>
|
||||
<TooltipProvider>
|
||||
<App />
|
||||
</TooltipProvider>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>
|
||||
)
|
||||
<React.StrictMode>
|
||||
<MantineProvider defaultColorScheme="dark">
|
||||
<Notifications />
|
||||
<App />
|
||||
</MantineProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue