diff --git a/lama_cleaner/app/src/App.tsx b/lama_cleaner/app/src/App.tsx index f23ff29..b771bdb 100644 --- a/lama_cleaner/app/src/App.tsx +++ b/lama_cleaner/app/src/App.tsx @@ -4,24 +4,11 @@ import { nanoid } from 'nanoid' import useInputImage from './hooks/useInputImage' import { themeState } from './components/Header/ThemeChanger' import Workspace from './components/Workspace' -import { - enableFileManagerState, - fileState, - isControlNetState, - isDisableModelSwitchState, - isEnableAutoSavingState, - toastState, -} from './store/Atoms' +import { fileState, serverConfigState, toastState } from './store/Atoms' import { keepGUIAlive } from './utils' import Header from './components/Header/Header' import useHotKey from './hooks/useHotkey' -import { - getEnableAutoSaving, - getEnableFileManager, - getIsControlNet, - getIsDisableModelSwitch, - isDesktop, -} from './adapters/inpainting' +import { getServerConfig, isDesktop } from './adapters/inpainting' const SUPPORTED_FILE_TYPE = [ 'image/jpeg', @@ -36,10 +23,7 @@ function App() { const [theme, setTheme] = useRecoilState(themeState) const setToastState = useSetRecoilState(toastState) const userInputImage = useInputImage() - const setIsDisableModelSwitch = useSetRecoilState(isDisableModelSwitchState) - const setEnableFileManager = useSetRecoilState(enableFileManagerState) - const setIsEnableAutoSavingState = useSetRecoilState(isEnableAutoSavingState) - const setIsControlNet = useSetRecoilState(isControlNetState) + const setServerConfigState = useSetRecoilState(serverConfigState) // Set Input Image useEffect(() => { @@ -58,38 +42,13 @@ function App() { }, []) useEffect(() => { - const fetchData = async () => { - const isDisable: string = await getIsDisableModelSwitch().then(res => - res.text() - ) - setIsDisableModelSwitch(isDisable === 'true') + const fetchServerConfig = async () => { + const serverConfig = await getServerConfig().then(res => res.json()) + console.log(serverConfig) + setServerConfigState(serverConfig) } - - fetchData() - - const fetchData2 = async () => { - const isEnabled = await getEnableFileManager().then(res => res.text()) - setEnableFileManager(isEnabled === 'true') - } - fetchData2() - - const fetchData3 = async () => { - const isEnabled = await getEnableAutoSaving().then(res => res.text()) - setIsEnableAutoSavingState(isEnabled === 'true') - } - fetchData3() - - const fetchData4 = async () => { - const isEnabled = await getIsControlNet().then(res => res.text()) - setIsControlNet(isEnabled === 'true') - } - fetchData4() - }, [ - setEnableFileManager, - setIsDisableModelSwitch, - setIsEnableAutoSavingState, - setIsControlNet, - ]) + fetchServerConfig() + }, []) // Dark Mode Hotkey useHotKey( diff --git a/lama_cleaner/app/src/adapters/inpainting.ts b/lama_cleaner/app/src/adapters/inpainting.ts index 954dab8..1fb6c9b 100644 --- a/lama_cleaner/app/src/adapters/inpainting.ts +++ b/lama_cleaner/app/src/adapters/inpainting.ts @@ -1,3 +1,4 @@ +import { PluginName } from '../components/Plugins/Plugins' import { Rect, Settings } from '../store/Atoms' import { dataURItoBlob, loadImage, srcToFile } from '../utils' @@ -116,26 +117,8 @@ export default async function inpaint( } } -export function getIsDisableModelSwitch() { - return fetch(`${API_ENDPOINT}/is_disable_model_switch`, { - method: 'GET', - }) -} - -export function getIsControlNet() { - return fetch(`${API_ENDPOINT}/is_controlnet`, { - method: 'GET', - }) -} - -export function getEnableFileManager() { - return fetch(`${API_ENDPOINT}/is_enable_file_manager`, { - method: 'GET', - }) -} - -export function getEnableAutoSaving() { - return fetch(`${API_ENDPOINT}/is_enable_auto_saving`, { +export function getServerConfig() { + return fetch(`${API_ENDPOINT}/server_config`, { method: 'GET', }) } @@ -167,20 +150,24 @@ export function modelDownloaded(name: string) { }) } -export async function postInteractiveSeg( +export async function runPlugin( + name: string, imageFile: File, - maskFile: File | null, - clicks: number[][] + maskFile?: File | null, + clicks?: number[][] ) { const fd = new FormData() + fd.append('name', name) fd.append('image', imageFile) - fd.append('clicks', JSON.stringify(clicks)) - if (maskFile !== null) { + if (clicks) { + fd.append('clicks', JSON.stringify(clicks)) + } + if (maskFile) { fd.append('mask', maskFile) } try { - const res = await fetch(`${API_ENDPOINT}/interactive_seg`, { + const res = await fetch(`${API_ENDPOINT}/run_plugin`, { method: 'POST', body: fd, }) @@ -255,12 +242,13 @@ export async function makeGif( ) { const cleanFile = await srcToFile(cleanImage.src, filename, mimeType) const fd = new FormData() - fd.append('origin_img', originFile) + fd.append('name', PluginName.MakeGIF) + fd.append('image', originFile) fd.append('clean_img', cleanFile) fd.append('filename', filename) try { - const res = await fetch(`${API_ENDPOINT}/make_gif`, { + const res = await fetch(`${API_ENDPOINT}/run_plugin`, { method: 'POST', body: fd, }) diff --git a/lama_cleaner/app/src/components/Editor/Editor.tsx b/lama_cleaner/app/src/components/Editor/Editor.tsx index 234d76a..80fbbb7 100644 --- a/lama_cleaner/app/src/components/Editor/Editor.tsx +++ b/lama_cleaner/app/src/components/Editor/Editor.tsx @@ -18,10 +18,7 @@ import { } from 'react-zoom-pan-pinch' import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil' import { useWindowSize, useKey, useKeyPressEvent } from 'react-use' -import inpaint, { - downloadToOutput, - postInteractiveSeg, -} from '../../adapters/inpainting' +import inpaint, { downloadToOutput, runPlugin } from '../../adapters/inpainting' import Button from '../shared/Button' import Slider from './Slider' import SizeSelector from './SizeSelector' @@ -50,6 +47,8 @@ import { isInteractiveSegRunningState, isInteractiveSegState, isPix2PixState, + isPluginRunningState, + isProcessingState, negativePropmtState, propmtState, runManuallyState, @@ -69,6 +68,7 @@ import FileSelect from '../FileSelect/FileSelect' import InteractiveSeg from '../InteractiveSeg/InteractiveSeg' import InteractiveSegConfirmActions from '../InteractiveSeg/ConfirmActions' import InteractiveSegReplaceModal from '../InteractiveSeg/ReplaceModal' +import { PluginName } from '../Plugins/Plugins' import MakeGIF from './MakeGIF' const TOOLBAR_SIZE = 200 @@ -118,13 +118,15 @@ export default function Editor() { const croperRect = useRecoilValue(croperState) const setToastState = useSetRecoilState(toastState) const [isInpainting, setIsInpainting] = useRecoilState(isInpaintingState) + const setIsPluginRunning = useSetRecoilState(isPluginRunningState) + const isProcessing = useRecoilValue(isProcessingState) const runMannually = useRecoilValue(runManuallyState) const isDiffusionModels = useRecoilValue(isDiffusionModelsState) const isPix2Pix = useRecoilValue(isPix2PixState) const [isInteractiveSeg, setIsInteractiveSeg] = useRecoilState( isInteractiveSegState ) - const [isInteractiveSegRunning, setIsInteractiveSegRunning] = useRecoilState( + const setIsInteractiveSegRunning = useSetRecoilState( isInteractiveSegRunningState ) @@ -538,6 +540,77 @@ export default function Editor() { } }, [runInpainting]) + const getCurrentRender = useCallback(async () => { + let targetFile = file + if (renders.length > 0) { + const lastRender = renders[renders.length - 1] + targetFile = await srcToFile(lastRender.currentSrc, file.name, file.type) + } + return targetFile + }, [file, renders]) + + useEffect(() => { + emitter.on(PluginName.InteractiveSeg, () => { + setIsInteractiveSeg(true) + if (interactiveSegMask !== null) { + setShowInteractiveSegModal(true) + } + }) + return () => { + emitter.off(PluginName.InteractiveSeg) + } + }) + + const runRenderablePlugin = useCallback( + async (name: string) => { + if (isProcessing) { + return + } + try { + // TODO 要不要加 undoCurrentLine?? + setIsPluginRunning(true) + const targetFile = await getCurrentRender() + const res = await runPlugin(name, targetFile) + if (!res) { + throw new Error('Something went wrong on server side.') + } + const { blob } = res + const newRender = new Image() + await loadImage(newRender, blob) + const newRenders = [...renders, newRender] + setRenders(newRenders) + } catch (e: any) { + setToastState({ + open: true, + desc: e.message ? e.message : e.toString(), + state: 'error', + duration: 3000, + }) + } finally { + setIsPluginRunning(false) + } + }, + [renders, setRenders, getCurrentRender, setIsPluginRunning, isProcessing] + ) + + useEffect(() => { + emitter.on(PluginName.RemoveBG, () => { + runRenderablePlugin(PluginName.RemoveBG) + }) + return () => { + emitter.off(PluginName.RemoveBG) + } + }, [runRenderablePlugin]) + + useEffect(() => { + emitter.on(PluginName.RealESRGAN, () => { + runRenderablePlugin(PluginName.RealESRGAN) + }) + return () => { + emitter.off(PluginName.RealESRGAN) + } + }, [runRenderablePlugin]) + const hadRunInpainting = () => { return renders.length !== 0 } @@ -759,13 +832,7 @@ export default function Editor() { } setIsInteractiveSegRunning(true) - - let targetFile = file - if (renders.length > 0) { - const lastRender = renders[renders.length - 1] - targetFile = await srcToFile(lastRender.currentSrc, file.name, file.type) - } - + const targetFile = await getCurrentRender() const prevMask = null // prev_mask seems to be not working better // if (tmpInteractiveSegMask !== null) { @@ -777,7 +844,12 @@ export default function Editor() { // } try { - const res = await postInteractiveSeg(targetFile, prevMask, newClicks) + const res = await runPlugin( + PluginName.InteractiveSeg.toString(), + targetFile, + prevMask, + newClicks + ) if (!res) { throw new Error('Something went wrong on server side.') } @@ -990,10 +1062,7 @@ export default function Editor() { ]) const disableUndo = () => { - if (isInteractiveSeg) { - return true - } - if (isInpainting) { + if (isProcessing) { return true } if (renders.length > 0) { @@ -1074,10 +1143,7 @@ export default function Editor() { ]) const disableRedo = () => { - if (isInteractiveSeg) { - return true - } - if (isInpainting) { + if (isProcessing) { return true } if (redoRenders.length > 0) { @@ -1185,20 +1251,6 @@ export default function Editor() { return undefined }, [showBrush, isPanning]) - useHotKey( - 'i', - () => { - if (!isInteractiveSeg && isOriginalLoaded) { - setIsInteractiveSeg(true) - if (interactiveSegMask !== null) { - setShowInteractiveSegModal(true) - } - } - }, - {}, - [isInteractiveSeg, interactiveSegMask, isOriginalLoaded] - ) - // Standard Hotkeys for Brush Size useHotKey('[', () => { setBrushSize((currentBrushSize: number) => { @@ -1370,11 +1422,7 @@ export default function Editor() { }} > {showOriginal && ( -
+ <> +
+ original + )} - - original
@@ -1467,6 +1516,7 @@ export default function Editor() { onMouseMove={onMouseMove} onMouseUp={onPointerUp} > + setShowRefBrush(false)} />
- -
- )} - - - + setShow(true) + setGifImg(null) + try { + const gif = await makeGif( + file, + renders[renders.length - 1], + file.name, + file.type + ) + if (gif) { + setGifImg(gif) + } + } catch (e: any) { + setToastState({ + open: true, + desc: e.message ? e.message : e.toString(), + state: 'error', + duration: 2000, + }) + setShow(false) + } + }) + return () => { + emitter.off(PluginName.MakeGIF) + } + }, [setGifImg, renders, file, setShow]) + + return ( + +
+ {gifImg ? ( + gif + ) : ( +
+ + Generating GIF... +
+ )} + + {gifImg && ( +
+ +
+ )} +
+
) } diff --git a/lama_cleaner/app/src/components/Plugins/Plugins.scss b/lama_cleaner/app/src/components/Plugins/Plugins.scss new file mode 100644 index 0000000..c1050ee --- /dev/null +++ b/lama_cleaner/app/src/components/Plugins/Plugins.scss @@ -0,0 +1,92 @@ +@use '../../styles/Mixins/' as *; + +.plugins { + position: absolute; + top: 68px; + left: 1rem; + padding: 0.1rem 0.3rem; + z-index: 4; + + border-radius: 0.8rem; + border-style: solid; + border-color: var(--border-color); + border-width: 1px; +} + +.plugins-trigger { + font-family: 'WorkSans', sans-serif; + font-size: 16px; + border: 0px; +} + +.plugins-content { + outline: none; + position: relative; + font-family: 'WorkSans', sans-serif; + font-size: 14px; + top: 8px; + left: 1rem; + padding: 0.8rem 0.5rem; + 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; + // } +} + +.negative-prompt { + all: unset; + border-width: 0; + border-radius: 0.5rem; + min-height: 150px; + max-width: 200px; + width: 100%; + padding: 12px 0.8rem; + outline: 1px solid var(--border-color); + + &:focus-visible { + border-width: 0; + outline: 1px solid var(--yellow-accent); + } + + &:-webkit-input-placeholder { + padding-top: 10px; + } + + &:-moz-input-placeholder { + padding-top: 10px; + } + + &:-ms-input-placeholder { + padding-top: 10px; + } +} + +.resize-title-tile { + width: 86px; + font-size: 0.5rem; + color: var(--text-color-gray); +} diff --git a/lama_cleaner/app/src/components/Plugins/Plugins.tsx b/lama_cleaner/app/src/components/Plugins/Plugins.tsx new file mode 100644 index 0000000..83a6596 --- /dev/null +++ b/lama_cleaner/app/src/components/Plugins/Plugins.tsx @@ -0,0 +1,91 @@ +import React, { FormEvent } from 'react' +import { useRecoilValue } from 'recoil' +import { CursorArrowRaysIcon, GifIcon } from '@heroicons/react/24/outline' +import { BoxModelIcon, MarginIcon, HobbyKnifeIcon } from '@radix-ui/react-icons' +import { useToggle } from 'react-use' +import * as PopoverPrimitive from '@radix-ui/react-popover' +import { + fileState, + isInpaintingState, + isPluginRunningState, + isProcessingState, + serverConfigState, +} from '../../store/Atoms' +import emitter from '../../event' +import Button from '../shared/Button' + +export enum PluginName { + RemoveBG = 'RemoveBG', + RealESRGAN = 'RealESRGAN', + InteractiveSeg = 'InteractiveSeg', + MakeGIF = 'MakeGIF', +} + +const pluginMap = { + [PluginName.RemoveBG]: { + IconClass: HobbyKnifeIcon, + showName: 'RemoveBG', + }, + [PluginName.RealESRGAN]: { + IconClass: BoxModelIcon, + showName: 'RealESRGAN 4x', + }, + [PluginName.InteractiveSeg]: { + IconClass: CursorArrowRaysIcon, + showName: 'Interactive Seg', + }, + [PluginName.MakeGIF]: { + IconClass: GifIcon, + showName: 'Make GIF', + }, +} + +const Plugins = () => { + const [open, toggleOpen] = useToggle(true) + const serverConfig = useRecoilValue(serverConfigState) + const file = useRecoilValue(fileState) + const isProcessing = useRecoilValue(isProcessingState) + + const onPluginClick = (pluginName: string) => { + if (isProcessing) { + return + } + emitter.emit(pluginName) + } + + const renderPlugins = () => { + return serverConfig.plugins.map((plugin: string) => { + const { IconClass } = pluginMap[plugin as PluginName] + return ( + + ) + }) + } + + return ( +
+ + toggleOpen()} + > + Plugins + + + + {renderPlugins()} + + + +
+ ) +} + +export default Plugins diff --git a/lama_cleaner/app/src/components/Settings/ModelSettingBlock.tsx b/lama_cleaner/app/src/components/Settings/ModelSettingBlock.tsx index 905979b..4778361 100644 --- a/lama_cleaner/app/src/components/Settings/ModelSettingBlock.tsx +++ b/lama_cleaner/app/src/components/Settings/ModelSettingBlock.tsx @@ -1,11 +1,9 @@ import React, { ReactNode, useEffect, useState } from 'react' import { useRecoilState, useRecoilValue } from 'recoil' -import { getIsDisableModelSwitch } from '../../adapters/inpainting' import { AIModel, CV2Flag, isDisableModelSwitchState, - SDSampler, settingState, } from '../../store/Atoms' import Selector from '../shared/Selector' diff --git a/lama_cleaner/app/src/components/SidePanel/SidePanel.scss b/lama_cleaner/app/src/components/SidePanel/SidePanel.scss index ad3e140..2e5ff9b 100644 --- a/lama_cleaner/app/src/components/SidePanel/SidePanel.scss +++ b/lama_cleaner/app/src/components/SidePanel/SidePanel.scss @@ -4,7 +4,7 @@ position: absolute; top: 68px; right: 1.5rem; - padding: 0.3rem 0.3rem; + padding: 0.1rem 0.3rem; z-index: 4; border-radius: 0.8rem; @@ -20,10 +20,11 @@ } .side-panel-content { + outline: none; position: relative; font-family: 'WorkSans', sans-serif; font-size: 14px; - top: 1rem; + top: 8px; right: 1.5rem; padding: 1rem 1rem; z-index: 9; diff --git a/lama_cleaner/app/src/components/Workspace.tsx b/lama_cleaner/app/src/components/Workspace.tsx index f1bf5b9..d26c68f 100644 --- a/lama_cleaner/app/src/components/Workspace.tsx +++ b/lama_cleaner/app/src/components/Workspace.tsx @@ -24,6 +24,7 @@ import SidePanel from './SidePanel/SidePanel' import PESidePanel from './SidePanel/PESidePanel' import FileManager from './FileManager/FileManager' import P2PSidePanel from './SidePanel/P2PSidePanel' +import Plugins from './Plugins/Plugins' const Workspace = () => { const setFile = useSetRecoilState(fileState) @@ -102,6 +103,7 @@ const Workspace = () => { {isSD ? : <>} {isPaintByExample ? : <>} {isPix2Pix ? : <>} + ({ @@ -72,6 +74,8 @@ export const appState = atom({ gifImage: undefined, brushSize: 40, isControlNet: false, + plugins: [], + isPluginRunning: false, }, }) @@ -97,6 +101,36 @@ export const isInpaintingState = selector({ }, }) +export const isPluginRunningState = selector({ + key: 'isPluginRunningState', + get: ({ get }) => { + const app = get(appState) + return app.isPluginRunning + }, + set: ({ get, set }, newValue: any) => { + const app = get(appState) + set(appState, { ...app, isPluginRunning: newValue }) + }, +}) + +export const serverConfigState = selector({ + key: 'serverConfigState', + get: ({ get }) => { + const app = get(appState) + return { + isControlNet: app.isControlNet, + isDisableModelSwitchState: app.isDisableModelSwitch, + isEnableAutoSaving: app.isEnableAutoSaving, + enableFileManager: app.enableFileManager, + plugins: app.plugins, + } + }, + set: ({ get, set }, newValue: any) => { + const app = get(appState) + set(appState, { ...app, ...newValue }) + }, +}) + export const brushSizeState = selector({ key: 'brushSizeState', get: ({ get }) => { @@ -217,6 +251,16 @@ export const isInteractiveSegRunningState = selector({ }, }) +export const isProcessingState = selector({ + key: 'isProcessingState', + get: ({ get }) => { + const app = get(appState) + return ( + app.isInteractiveSegRunning || app.isPluginRunning || app.isInpainting + ) + }, +}) + export const interactiveSegClicksState = selector({ key: 'interactiveSegClicksState', get: ({ get }) => { diff --git a/lama_cleaner/app/src/styles/_index.scss b/lama_cleaner/app/src/styles/_index.scss index 3f3f6fe..bc3ac9f 100644 --- a/lama_cleaner/app/src/styles/_index.scss +++ b/lama_cleaner/app/src/styles/_index.scss @@ -15,6 +15,7 @@ @use '../components/Shortcuts/Shortcuts'; @use '../components/Settings/Settings.scss'; @use '../components/SidePanel/SidePanel.scss'; +@use '../components/Plugins/Plugins.scss'; @use '../components/Croper/Croper.scss'; @use '../components/InteractiveSeg/InteractiveSeg.scss'; diff --git a/lama_cleaner/parse_args.py b/lama_cleaner/parse_args.py index ed55dc3..7c7d4bd 100644 --- a/lama_cleaner/parse_args.py +++ b/lama_cleaner/parse_args.py @@ -94,6 +94,11 @@ def parse_args(): parser.add_argument( "--realesrgan-device", default="cpu", type=str, choices=["cpu", "cuda"] ) + parser.add_argument( + "--enable-gif", + action="store_true", + help="Enable GIF plugin", + ) ######### # useless args diff --git a/lama_cleaner/plugins/__init__.py b/lama_cleaner/plugins/__init__.py index 711b023..d912a0a 100644 --- a/lama_cleaner/plugins/__init__.py +++ b/lama_cleaner/plugins/__init__.py @@ -1,3 +1,3 @@ from .interactive_seg import InteractiveSeg, Click from .remove_bg import RemoveBG -from .upscale import RealESRGANUpscaler +from .realesrgan import RealESRGANUpscaler diff --git a/lama_cleaner/make_gif.py b/lama_cleaner/plugins/gif.py similarity index 69% rename from lama_cleaner/make_gif.py rename to lama_cleaner/plugins/gif.py index 12a213f..e901327 100644 --- a/lama_cleaner/make_gif.py +++ b/lama_cleaner/plugins/gif.py @@ -1,9 +1,10 @@ import io import math -from pathlib import Path from PIL import Image, ImageDraw +from lama_cleaner.helper import load_img + def keep_ratio_resize(img, size, resample=Image.BILINEAR): if img.width > img.height: @@ -33,16 +34,20 @@ def cubic_bezier(p1, p2, duration: int, frames: int): x3, y3 = (1, 1) def cal_y(t): - return math.pow(1 - t, 3) * y0 + \ - 3 * math.pow(1 - t, 2) * t * y1 + \ - 3 * (1 - t) * math.pow(t, 2) * y2 + \ - math.pow(t, 3) * y3 + return ( + math.pow(1 - t, 3) * y0 + + 3 * math.pow(1 - t, 2) * t * y1 + + 3 * (1 - t) * math.pow(t, 2) * y2 + + math.pow(t, 3) * y3 + ) def cal_x(t): - return math.pow(1 - t, 3) * x0 + \ - 3 * math.pow(1 - t, 2) * t * x1 + \ - 3 * (1 - t) * math.pow(t, 2) * x2 + \ - math.pow(t, 3) * x3 + return ( + math.pow(1 - t, 3) * x0 + + 3 * math.pow(1 - t, 2) * t * x1 + + 3 * (1 - t) * math.pow(t, 2) * x2 + + math.pow(t, 3) * x3 + ) res = [] for t in range(0, 1 * frames, duration): @@ -58,7 +63,7 @@ def make_compare_gif( src_img: Image.Image, max_side_length: int = 600, splitter_width: int = 5, - splitter_color=(255, 203, 0, int(255 * 0.73)) + splitter_color=(255, 203, 0, int(255 * 0.73)), ): if clean_img.size != src_img.size: clean_img = clean_img.resize(src_img.size, Image.BILINEAR) @@ -79,7 +84,7 @@ def make_compare_gif( images = [] for i in range(num_frames): - new_frame = Image.new('RGB', (width, height)) + new_frame = Image.new("RGB", (width, height)) new_frame.paste(clean_img, (0, 0)) left = int(cubic_bezier_points[i][0] * width) @@ -88,7 +93,9 @@ def make_compare_gif( if i != num_frames - 1: # draw a yellow splitter on the edge of the cropped image draw = ImageDraw.Draw(new_frame) - draw.line([(left, 0), (left, height)], width=splitter_width, fill=splitter_color) + draw.line( + [(left, 0), (left, height)], width=splitter_width, fill=splitter_color + ) images.append(new_frame) for i in range(10): @@ -97,7 +104,7 @@ def make_compare_gif( cubic_bezier_points.reverse() # Generate images to make Gif from left to right for i in range(num_frames): - new_frame = Image.new('RGB', (width, height)) + new_frame = Image.new("RGB", (width, height)) new_frame.paste(src_img, (0, 0)) right = int(cubic_bezier_points[i][0] * width) @@ -106,7 +113,9 @@ def make_compare_gif( if i != num_frames - 1: # draw a yellow splitter on the edge of the cropped image draw = ImageDraw.Draw(new_frame) - draw.line([(right, 0), (right, height)], width=splitter_width, fill=splitter_color) + draw.line( + [(right, 0), (right, height)], width=splitter_width, fill=splitter_color + ) images.append(new_frame) images.append(clean_img) @@ -114,12 +123,25 @@ def make_compare_gif( img_byte_arr = io.BytesIO() clean_img.save( img_byte_arr, - format='GIF', + format="GIF", save_all=True, include_color_table=True, append_images=images, optimize=False, duration=duration_per_frame, - loop=0 + loop=0, ) return img_byte_arr.getvalue() + + +class MakeGIF: + name = "MakeGIF" + + def __call__(self, rgb_np_img, files, form): + origin_image = rgb_np_img + clean_image_bytes = files["clean_img"].read() + clean_image, _ = load_img(clean_image_bytes) + gif_bytes = make_compare_gif( + Image.fromarray(origin_image), Image.fromarray(clean_image) + ) + return gif_bytes diff --git a/lama_cleaner/plugins/upscale.py b/lama_cleaner/plugins/realesrgan.py similarity index 97% rename from lama_cleaner/plugins/upscale.py rename to lama_cleaner/plugins/realesrgan.py index e4eb8bc..2758291 100644 --- a/lama_cleaner/plugins/upscale.py +++ b/lama_cleaner/plugins/realesrgan.py @@ -37,7 +37,7 @@ class RealESRGANUpscaler: def __call__(self, rgb_np_img, files, form): bgr_np_img = cv2.cvtColor(rgb_np_img, cv2.COLOR_RGB2BGR) - scale = float(form["scale"]) + scale = 4 return self.forward(bgr_np_img, scale) def forward(self, bgr_np_img, scale: float): diff --git a/lama_cleaner/server.py b/lama_cleaner/server.py index a19b027..d6b49f0 100644 --- a/lama_cleaner/server.py +++ b/lama_cleaner/server.py @@ -17,10 +17,10 @@ import numpy as np from loguru import logger from lama_cleaner.const import SD15_MODELS -from lama_cleaner.make_gif import make_compare_gif from lama_cleaner.model.utils import torch_gc from lama_cleaner.model_manager import ModelManager from lama_cleaner.plugins import InteractiveSeg, RemoveBG, RealESRGANUpscaler +from lama_cleaner.plugins.gif import MakeGIF from lama_cleaner.schema import Config from lama_cleaner.file_manager import FileManager @@ -318,11 +318,10 @@ def process(): return response -@app.route("/run_plugin/", methods=["POST"]) +@app.route("/run_plugin", methods=["POST"]) def run_plugin(): form = request.form files = request.files - name = form["name"] if name not in plugins: return "Plugin not found", 500 @@ -335,18 +334,33 @@ def run_plugin(): logger.info(f"{name} process time: {(time.time() - start) * 1000}ms") torch_gc() - response = make_response( - send_file( - io.BytesIO(numpy_to_bytes(res, "png")), - mimetype=f"image/png", + if name == MakeGIF.name: + filename = form["filename"] + return send_file( + io.BytesIO(res), + mimetype="image/gif", + as_attachment=True, + attachment_filename=filename, + ) + else: + response = make_response( + send_file( + io.BytesIO(numpy_to_bytes(res, "png")), + mimetype=f"image/png", + ) ) - ) return response -@app.route("/plugins/", methods=["GET"]) -def get_plugins(): - return list(plugins.keys()), 200 +@app.route("/server_config", methods=["GET"]) +def get_server_config(): + return { + "isControlNet": is_controlnet, + "isDisableModelSwitchState": is_disable_model_switch, + "isEnableAutoSaving": is_enable_file_manager, + "enableFileManager": is_enable_auto_saving, + "plugins": list(plugins.keys()), + }, 200 @app.route("/model") @@ -354,30 +368,6 @@ def current_model(): return model.name, 200 -@app.route("/is_controlnet") -def get_is_controlnet(): - res = "true" if is_controlnet else "false" - return res, 200 - - -@app.route("/is_disable_model_switch") -def get_is_disable_model_switch(): - res = "true" if is_disable_model_switch else "false" - return res, 200 - - -@app.route("/is_enable_file_manager") -def get_is_enable_file_manager(): - res = "true" if is_enable_file_manager else "false" - return res, 200 - - -@app.route("/is_enable_auto_saving") -def get_is_enable_auto_saving(): - res = "true" if is_enable_auto_saving else "false" - return res, 200 - - @app.route("/model_downloaded/") def model_downloaded(name): return str(model.is_downloaded(name)), 200 @@ -435,6 +425,9 @@ def build_plugins(args): if args.enable_realesrgan: logger.info(f"Initialize {RealESRGANUpscaler.name} plugin") plugins[RealESRGANUpscaler.name] = RealESRGANUpscaler(args.realesrgan_device) + if args.enable_gif: + logger.info(f"Initialize GIF plugin") + plugins[MakeGIF.name] = MakeGIF() def main(args):