add file manager
This commit is contained in:
parent
b847ded828
commit
2dd95be90d
@ -30,7 +30,8 @@
|
|||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-feather": "^2.0.10",
|
"react-feather": "^2.0.10",
|
||||||
"react-hotkeys-hook": "^3.4.7",
|
"react-hotkeys-hook": "^3.4.7",
|
||||||
"react-scripts": "4.0.3",
|
"react-photo-album": "^2.0.0",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
"react-use": "^17.3.1",
|
"react-use": "^17.3.1",
|
||||||
"react-zoom-pan-pinch": "^2.1.3",
|
"react-zoom-pan-pinch": "^2.1.3",
|
||||||
"recoil": "^0.6.1",
|
"recoil": "^0.6.1",
|
||||||
@ -38,7 +39,7 @@
|
|||||||
"typescript": "4.x"
|
"typescript": "4.x"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "cross-env GENERATE_SOURCEMAP=false react-scripts start",
|
||||||
"build": "cross-env GENERATE_SOURCEMAP=false react-scripts build",
|
"build": "cross-env GENERATE_SOURCEMAP=false react-scripts build",
|
||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject"
|
||||||
@ -65,8 +66,8 @@
|
|||||||
"eslint-plugin-import": "^2.25.2",
|
"eslint-plugin-import": "^2.25.2",
|
||||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"eslint-plugin-react": "^7.26.1",
|
"eslint-plugin-react": "^7.27.1",
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
"eslint-plugin-react-hooks": "^4.3.0",
|
||||||
"prettier": "^2.4.1",
|
"prettier": "^2.4.1",
|
||||||
"sass": "^1.49.9"
|
"sass": "^1.49.9"
|
||||||
}
|
}
|
||||||
|
@ -164,3 +164,16 @@ export async function postInteractiveSeg(
|
|||||||
throw new Error(`Something went wrong: ${error}`)
|
throw new Error(`Something went wrong: ${error}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getMediaFile(filename: string) {
|
||||||
|
const res = await fetch(`${API_ENDPOINT}/media/${filename}`, {
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
if (res.ok) {
|
||||||
|
const blob = await res.blob()
|
||||||
|
const file = new File([blob], filename)
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
const errMsg = await res.text()
|
||||||
|
throw new Error(errMsg)
|
||||||
|
}
|
||||||
|
27
lama_cleaner/app/src/components/FileManager/FileManager.scss
Normal file
27
lama_cleaner/app/src/components/FileManager/FileManager.scss
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
.file-manager-modal {
|
||||||
|
color: var(--text-color);
|
||||||
|
height: 90%;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-manager {
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-photo-album.react-photo-album--columns {
|
||||||
|
height: 80vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-photo-album--photo {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
|
||||||
|
// transform-origin: 0 0;
|
||||||
|
transition: transform 0.25s, visibility 0.25s ease-in;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
transform: scale(1.01);
|
||||||
|
}
|
||||||
|
}
|
90
lama_cleaner/app/src/components/FileManager/FileManager.tsx
Normal file
90
lama_cleaner/app/src/components/FileManager/FileManager.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import React, { ReactNode, useEffect, useMemo, useState } from 'react'
|
||||||
|
import PhotoAlbum, { RenderPhoto } from 'react-photo-album'
|
||||||
|
import Modal from '../shared/Modal'
|
||||||
|
|
||||||
|
interface Photo {
|
||||||
|
src: string
|
||||||
|
height: number
|
||||||
|
width: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Filename {
|
||||||
|
name: string
|
||||||
|
height: number
|
||||||
|
width: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderPhoto: RenderPhoto = ({
|
||||||
|
layout,
|
||||||
|
layoutOptions,
|
||||||
|
imageProps: { alt, style, ...restImageProps },
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
boxSizing: 'content-box',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt={alt}
|
||||||
|
style={{ ...style, width: '100%', padding: 0 }}
|
||||||
|
{...restImageProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
show: boolean
|
||||||
|
onClose: () => void
|
||||||
|
onPhotoClick: (filename: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FileManager(props: Props) {
|
||||||
|
const { show, onClose, onPhotoClick } = props
|
||||||
|
const [filenames, setFileNames] = useState<Filename[]>([])
|
||||||
|
|
||||||
|
const onClick = ({ index }: { index: number }) => {
|
||||||
|
onPhotoClick(filenames[index].name)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
const res = await fetch('/medias')
|
||||||
|
if (res.ok) {
|
||||||
|
const newFilenames = await res.json()
|
||||||
|
setFileNames(newFilenames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchData()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const photos = useMemo(() => {
|
||||||
|
return filenames.map((filename: Filename) => {
|
||||||
|
const width = 256
|
||||||
|
const height = filename.height * (width / filename.width)
|
||||||
|
const src = `/media_thumbnail/${filename.name}?width=${width}&height=${height}`
|
||||||
|
return { src, height, width }
|
||||||
|
})
|
||||||
|
}, [filenames])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
onClose={onClose}
|
||||||
|
title="Files"
|
||||||
|
className="file-manager-modal"
|
||||||
|
show={show}
|
||||||
|
>
|
||||||
|
<div className="file-manager">
|
||||||
|
<PhotoAlbum
|
||||||
|
layout="columns"
|
||||||
|
photos={photos}
|
||||||
|
renderPhoto={renderPhoto}
|
||||||
|
spacing={6}
|
||||||
|
padding={4}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { ArrowUpTrayIcon } from '@heroicons/react/24/outline'
|
import { FolderIcon, PhotoIcon } from '@heroicons/react/24/outline'
|
||||||
import { PlayIcon } from '@radix-ui/react-icons'
|
import { PlayIcon } from '@radix-ui/react-icons'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil'
|
import { useRecoilState, useRecoilValue } from 'recoil'
|
||||||
@ -9,6 +9,7 @@ import {
|
|||||||
isSDState,
|
isSDState,
|
||||||
maskState,
|
maskState,
|
||||||
runManuallyState,
|
runManuallyState,
|
||||||
|
showFileManagerState,
|
||||||
} from '../../store/Atoms'
|
} from '../../store/Atoms'
|
||||||
import Button from '../shared/Button'
|
import Button from '../shared/Button'
|
||||||
import Shortcuts from '../Shortcuts/Shortcuts'
|
import Shortcuts from '../Shortcuts/Shortcuts'
|
||||||
@ -18,6 +19,7 @@ import PromptInput from './PromptInput'
|
|||||||
import CoffeeIcon from '../CoffeeIcon/CoffeeIcon'
|
import CoffeeIcon from '../CoffeeIcon/CoffeeIcon'
|
||||||
import emitter, { EVENT_CUSTOM_MASK } from '../../event'
|
import emitter, { EVENT_CUSTOM_MASK } from '../../event'
|
||||||
import { useImage } from '../../utils'
|
import { useImage } from '../../utils'
|
||||||
|
import useHotKey from '../../hooks/useHotkey'
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const isInpainting = useRecoilValue(isInpaintingState)
|
const isInpainting = useRecoilValue(isInpaintingState)
|
||||||
@ -29,6 +31,17 @@ const Header = () => {
|
|||||||
const isSD = useRecoilValue(isSDState)
|
const isSD = useRecoilValue(isSDState)
|
||||||
const runManually = useRecoilValue(runManuallyState)
|
const runManually = useRecoilValue(runManuallyState)
|
||||||
const [openMaskPopover, setOpenMaskPopover] = useState(false)
|
const [openMaskPopover, setOpenMaskPopover] = useState(false)
|
||||||
|
const [showFileManager, setShowFileManager] =
|
||||||
|
useRecoilState(showFileManagerState)
|
||||||
|
|
||||||
|
useHotKey(
|
||||||
|
'f',
|
||||||
|
() => {
|
||||||
|
setShowFileManager(!showFileManager)
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
[showFileManager]
|
||||||
|
)
|
||||||
|
|
||||||
const renderHeader = () => {
|
const renderHeader = () => {
|
||||||
return (
|
return (
|
||||||
@ -41,10 +54,20 @@ const Header = () => {
|
|||||||
gap: 8,
|
gap: 8,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Button
|
||||||
|
icon={<FolderIcon />}
|
||||||
|
style={{ border: 0 }}
|
||||||
|
toolTip="Open File Manager"
|
||||||
|
tooltipPosition="bottom"
|
||||||
|
onClick={() => {
|
||||||
|
setShowFileManager(true)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<label htmlFor={uploadElemId}>
|
<label htmlFor={uploadElemId}>
|
||||||
<Button
|
<Button
|
||||||
icon={<ArrowUpTrayIcon />}
|
icon={<PhotoIcon />}
|
||||||
style={{ border: 0 }}
|
style={{ border: 0, gap: 0 }}
|
||||||
disabled={isInpainting}
|
disabled={isInpainting}
|
||||||
toolTip="Upload image"
|
toolTip="Upload image"
|
||||||
tooltipPosition="bottom"
|
tooltipPosition="bottom"
|
||||||
@ -62,7 +85,6 @@ const Header = () => {
|
|||||||
}}
|
}}
|
||||||
accept="image/png, image/jpeg"
|
accept="image/png, image/jpeg"
|
||||||
/>
|
/>
|
||||||
Image
|
|
||||||
</Button>
|
</Button>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
@ -67,6 +67,7 @@ export default function ShortcutsModal() {
|
|||||||
<ShortCut content="Toggle Dark Mode" keys={['Shift', 'D']} />
|
<ShortCut content="Toggle Dark Mode" keys={['Shift', 'D']} />
|
||||||
<ShortCut content="Toggle Hotkeys Dialog" keys={['H']} />
|
<ShortCut content="Toggle Hotkeys Dialog" keys={['H']} />
|
||||||
<ShortCut content="Toggle Settings Dialog" keys={['S']} />
|
<ShortCut content="Toggle Settings Dialog" keys={['S']} />
|
||||||
|
<ShortCut content="Toggle File Manager" keys={['F']} />
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil'
|
import { useRecoilState, useRecoilValue } from 'recoil'
|
||||||
import Editor from './Editor/Editor'
|
import Editor from './Editor/Editor'
|
||||||
import ShortcutsModal from './Shortcuts/ShortcutsModal'
|
import ShortcutsModal from './Shortcuts/ShortcutsModal'
|
||||||
@ -10,15 +10,18 @@ import {
|
|||||||
isPaintByExampleState,
|
isPaintByExampleState,
|
||||||
isSDState,
|
isSDState,
|
||||||
settingState,
|
settingState,
|
||||||
|
showFileManagerState,
|
||||||
toastState,
|
toastState,
|
||||||
} from '../store/Atoms'
|
} from '../store/Atoms'
|
||||||
import {
|
import {
|
||||||
currentModel,
|
currentModel,
|
||||||
|
getMediaFile,
|
||||||
modelDownloaded,
|
modelDownloaded,
|
||||||
switchModel,
|
switchModel,
|
||||||
} from '../adapters/inpainting'
|
} from '../adapters/inpainting'
|
||||||
import SidePanel from './SidePanel/SidePanel'
|
import SidePanel from './SidePanel/SidePanel'
|
||||||
import PESidePanel from './SidePanel/PESidePanel'
|
import PESidePanel from './SidePanel/PESidePanel'
|
||||||
|
import FileManager from './FileManager/FileManager'
|
||||||
|
|
||||||
const Workspace = () => {
|
const Workspace = () => {
|
||||||
const [file, setFile] = useRecoilState(fileState)
|
const [file, setFile] = useRecoilState(fileState)
|
||||||
@ -27,6 +30,9 @@ const Workspace = () => {
|
|||||||
const isSD = useRecoilValue(isSDState)
|
const isSD = useRecoilValue(isSDState)
|
||||||
const isPaintByExample = useRecoilValue(isPaintByExampleState)
|
const isPaintByExample = useRecoilValue(isPaintByExampleState)
|
||||||
|
|
||||||
|
const [showFileManager, setShowFileManager] =
|
||||||
|
useRecoilState(showFileManagerState)
|
||||||
|
|
||||||
const onSettingClose = async () => {
|
const onSettingClose = async () => {
|
||||||
const curModel = await currentModel().then(res => res.text())
|
const curModel = await currentModel().then(res => res.text())
|
||||||
if (curModel === settings.model) {
|
if (curModel === settings.model) {
|
||||||
@ -92,6 +98,17 @@ const Workspace = () => {
|
|||||||
<>
|
<>
|
||||||
{isSD ? <SidePanel /> : <></>}
|
{isSD ? <SidePanel /> : <></>}
|
||||||
{isPaintByExample ? <PESidePanel /> : <></>}
|
{isPaintByExample ? <PESidePanel /> : <></>}
|
||||||
|
<FileManager
|
||||||
|
show={showFileManager}
|
||||||
|
onClose={() => {
|
||||||
|
setShowFileManager(false)
|
||||||
|
}}
|
||||||
|
onPhotoClick={async (filename: string) => {
|
||||||
|
const newFile = await getMediaFile(filename)
|
||||||
|
setFile(newFile)
|
||||||
|
setShowFileManager(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Editor />
|
<Editor />
|
||||||
<SettingModal onClose={onSettingClose} />
|
<SettingModal onClose={onSettingClose} />
|
||||||
<ShortcutsModal />
|
<ShortcutsModal />
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
grid-auto-rows: max-content;
|
grid-auto-rows: max-content;
|
||||||
row-gap: 1rem;
|
row-gap: 1rem;
|
||||||
place-self: center;
|
place-self: center;
|
||||||
padding: 2rem;
|
padding: 25px;
|
||||||
border-radius: 0.95rem;
|
border-radius: 0.95rem;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
|
@ -41,6 +41,7 @@ interface AppState {
|
|||||||
isInteractiveSeg: boolean
|
isInteractiveSeg: boolean
|
||||||
isInteractiveSegRunning: boolean
|
isInteractiveSegRunning: boolean
|
||||||
interactiveSegClicks: number[][]
|
interactiveSegClicks: number[][]
|
||||||
|
showFileManager: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const appState = atom<AppState>({
|
export const appState = atom<AppState>({
|
||||||
@ -53,6 +54,7 @@ export const appState = atom<AppState>({
|
|||||||
isInteractiveSeg: false,
|
isInteractiveSeg: false,
|
||||||
isInteractiveSegRunning: false,
|
isInteractiveSegRunning: false,
|
||||||
interactiveSegClicks: [],
|
interactiveSegClicks: [],
|
||||||
|
showFileManager: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -78,6 +80,18 @@ export const isInpaintingState = selector({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const showFileManagerState = selector({
|
||||||
|
key: 'showFileManager',
|
||||||
|
get: ({ get }) => {
|
||||||
|
const app = get(appState)
|
||||||
|
return app.showFileManager
|
||||||
|
},
|
||||||
|
set: ({ get, set }, newValue: any) => {
|
||||||
|
const app = get(appState)
|
||||||
|
set(appState, { ...app, showFileManager: newValue })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
export const fileState = selector({
|
export const fileState = selector({
|
||||||
key: 'fileState',
|
key: 'fileState',
|
||||||
get: ({ get }) => {
|
get: ({ get }) => {
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
// App
|
// App
|
||||||
@use './App';
|
@use './App';
|
||||||
@use '../components/Editor/Editor';
|
@use '../components/Editor/Editor';
|
||||||
|
@use '../components/FileManager/FileManager';
|
||||||
@use '../components/LandingPage/LandingPage';
|
@use '../components/LandingPage/LandingPage';
|
||||||
@use '../components/Header/Header';
|
@use '../components/Header/Header';
|
||||||
@use '../components/Header/PromptInput';
|
@use '../components/Header/PromptInput';
|
||||||
|
File diff suppressed because it is too large
Load Diff
1
lama_cleaner/file_manager/__init__.py
Normal file
1
lama_cleaner/file_manager/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .file_manager import FileManager
|
186
lama_cleaner/file_manager/file_manager.py
Normal file
186
lama_cleaner/file_manager/file_manager.py
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
# Copy from https://github.com/silentsokolov/flask-thumbnails/blob/master/flask_thumbnails/thumbnail.py
|
||||||
|
import os
|
||||||
|
from functools import lru_cache
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from PIL import Image, ImageOps, PngImagePlugin
|
||||||
|
LARGE_ENOUGH_NUMBER = 100
|
||||||
|
PngImagePlugin.MAX_TEXT_CHUNK = LARGE_ENOUGH_NUMBER * (1024**2)
|
||||||
|
from .storage_backends import FilesystemStorageBackend
|
||||||
|
from .utils import aspect_to_string, generate_filename, glob_img
|
||||||
|
|
||||||
|
|
||||||
|
class FileManager:
|
||||||
|
def __init__(self, app=None):
|
||||||
|
self.app = app
|
||||||
|
self._default_root_directory = "media"
|
||||||
|
self._default_thumbnail_directory = "media"
|
||||||
|
self._default_root_url = "/"
|
||||||
|
self._default_thumbnail_root_url = "/"
|
||||||
|
self._default_format = "JPEG"
|
||||||
|
|
||||||
|
if app is not None:
|
||||||
|
self.init_app(app)
|
||||||
|
|
||||||
|
def init_app(self, app):
|
||||||
|
if self.app is None:
|
||||||
|
self.app = app
|
||||||
|
app.thumbnail_instance = self
|
||||||
|
|
||||||
|
if not hasattr(app, "extensions"):
|
||||||
|
app.extensions = {}
|
||||||
|
|
||||||
|
if "thumbnail" in app.extensions:
|
||||||
|
raise RuntimeError("Flask-thumbnail extension already initialized")
|
||||||
|
|
||||||
|
app.extensions["thumbnail"] = self
|
||||||
|
|
||||||
|
app.config.setdefault("THUMBNAIL_MEDIA_ROOT", self._default_root_directory)
|
||||||
|
app.config.setdefault("THUMBNAIL_MEDIA_THUMBNAIL_ROOT", self._default_thumbnail_directory)
|
||||||
|
app.config.setdefault("THUMBNAIL_MEDIA_URL", self._default_root_url)
|
||||||
|
app.config.setdefault("THUMBNAIL_MEDIA_THUMBNAIL_URL", self._default_thumbnail_root_url)
|
||||||
|
app.config.setdefault("THUMBNAIL_DEFAULT_FORMAT", self._default_format)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def root_directory(self):
|
||||||
|
path = self.app.config["THUMBNAIL_MEDIA_ROOT"]
|
||||||
|
|
||||||
|
if os.path.isabs(path):
|
||||||
|
return path
|
||||||
|
else:
|
||||||
|
return os.path.join(self.app.root_path, path)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def thumbnail_directory(self):
|
||||||
|
path = self.app.config["THUMBNAIL_MEDIA_THUMBNAIL_ROOT"]
|
||||||
|
|
||||||
|
if os.path.isabs(path):
|
||||||
|
return path
|
||||||
|
else:
|
||||||
|
return os.path.join(self.app.root_path, path)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def root_url(self):
|
||||||
|
return self.app.config["THUMBNAIL_MEDIA_URL"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
@lru_cache()
|
||||||
|
def media_names(self):
|
||||||
|
names = sorted([it.name for it in glob_img(self.root_directory)])
|
||||||
|
res = []
|
||||||
|
for name in names:
|
||||||
|
img = Image.open(os.path.join(self.root_directory, name))
|
||||||
|
res.append({"name": name, "height": img.height, "width": img.width})
|
||||||
|
return res
|
||||||
|
|
||||||
|
@property
|
||||||
|
def thumbnail_url(self):
|
||||||
|
return self.app.config["THUMBNAIL_MEDIA_THUMBNAIL_URL"]
|
||||||
|
|
||||||
|
def get_thumbnail(self, original_filename, width, height, **options):
|
||||||
|
storage = FilesystemStorageBackend(self.app)
|
||||||
|
crop = options.get("crop", "fit")
|
||||||
|
background = options.get("background")
|
||||||
|
quality = options.get("quality", 90)
|
||||||
|
|
||||||
|
original_path, original_filename = os.path.split(original_filename)
|
||||||
|
original_filepath = os.path.join(self.root_directory, original_path, original_filename)
|
||||||
|
image = Image.open(BytesIO(storage.read(original_filepath)))
|
||||||
|
|
||||||
|
# keep ratio resize
|
||||||
|
if width is not None:
|
||||||
|
height = int(image.height * width / image.width)
|
||||||
|
else:
|
||||||
|
width = int(image.width * height / image.height)
|
||||||
|
|
||||||
|
thumbnail_size = (width, height)
|
||||||
|
|
||||||
|
thumbnail_filename = generate_filename(
|
||||||
|
original_filename, aspect_to_string(thumbnail_size), crop, background, quality
|
||||||
|
)
|
||||||
|
|
||||||
|
thumbnail_filepath = os.path.join(
|
||||||
|
self.thumbnail_directory, original_path, thumbnail_filename
|
||||||
|
)
|
||||||
|
thumbnail_url = os.path.join(self.thumbnail_url, original_path, thumbnail_filename)
|
||||||
|
|
||||||
|
if storage.exists(thumbnail_filepath):
|
||||||
|
return thumbnail_url, (width, height)
|
||||||
|
|
||||||
|
try:
|
||||||
|
image.load()
|
||||||
|
except (IOError, OSError):
|
||||||
|
self.app.logger.warning("Thumbnail not load image: %s", original_filepath)
|
||||||
|
return thumbnail_url, (width, height)
|
||||||
|
|
||||||
|
# get original image format
|
||||||
|
options["format"] = options.get("format", image.format)
|
||||||
|
|
||||||
|
image = self._create_thumbnail(image, thumbnail_size, crop, background=background)
|
||||||
|
|
||||||
|
raw_data = self.get_raw_data(image, **options)
|
||||||
|
storage.save(thumbnail_filepath, raw_data)
|
||||||
|
|
||||||
|
return thumbnail_url, (width, height)
|
||||||
|
|
||||||
|
def get_raw_data(self, image, **options):
|
||||||
|
data = {
|
||||||
|
"format": self._get_format(image, **options),
|
||||||
|
"quality": options.get("quality", 90),
|
||||||
|
}
|
||||||
|
|
||||||
|
_file = BytesIO()
|
||||||
|
image.save(_file, **data)
|
||||||
|
return _file.getvalue()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def colormode(image, colormode="RGB"):
|
||||||
|
if colormode == "RGB" or colormode == "RGBA":
|
||||||
|
if image.mode == "RGBA":
|
||||||
|
return image
|
||||||
|
if image.mode == "LA":
|
||||||
|
return image.convert("RGBA")
|
||||||
|
return image.convert(colormode)
|
||||||
|
|
||||||
|
if colormode == "GRAY":
|
||||||
|
return image.convert("L")
|
||||||
|
|
||||||
|
return image.convert(colormode)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def background(original_image, color=0xFF):
|
||||||
|
size = (max(original_image.size),) * 2
|
||||||
|
image = Image.new("L", size, color)
|
||||||
|
image.paste(
|
||||||
|
original_image,
|
||||||
|
tuple(map(lambda x: (x[0] - x[1]) / 2, zip(size, original_image.size))),
|
||||||
|
)
|
||||||
|
|
||||||
|
return image
|
||||||
|
|
||||||
|
def _get_format(self, image, **options):
|
||||||
|
if options.get("format"):
|
||||||
|
return options.get("format")
|
||||||
|
if image.format:
|
||||||
|
return image.format
|
||||||
|
|
||||||
|
return self.app.config["THUMBNAIL_DEFAULT_FORMAT"]
|
||||||
|
|
||||||
|
def _create_thumbnail(self, image, size, crop="fit", background=None):
|
||||||
|
try:
|
||||||
|
resample = Image.Resampling.LANCZOS
|
||||||
|
except AttributeError: # pylint: disable=raise-missing-from
|
||||||
|
resample = Image.ANTIALIAS
|
||||||
|
|
||||||
|
if crop == "fit":
|
||||||
|
image = ImageOps.fit(image, size, resample)
|
||||||
|
else:
|
||||||
|
image = image.copy()
|
||||||
|
image.thumbnail(size, resample=resample)
|
||||||
|
|
||||||
|
if background is not None:
|
||||||
|
image = self.background(image)
|
||||||
|
|
||||||
|
image = self.colormode(image)
|
||||||
|
|
||||||
|
return image
|
46
lama_cleaner/file_manager/storage_backends.py
Normal file
46
lama_cleaner/file_manager/storage_backends.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Copy from https://github.com/silentsokolov/flask-thumbnails/blob/master/flask_thumbnails/storage_backends.py
|
||||||
|
import errno
|
||||||
|
import os
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
|
class BaseStorageBackend(ABC):
|
||||||
|
def __init__(self, app=None):
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def read(self, filepath, mode="rb", **kwargs):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def exists(self, filepath):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def save(self, filepath, data):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class FilesystemStorageBackend(BaseStorageBackend):
|
||||||
|
def read(self, filepath, mode="rb", **kwargs):
|
||||||
|
with open(filepath, mode) as f: # pylint: disable=unspecified-encoding
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
def exists(self, filepath):
|
||||||
|
return os.path.exists(filepath)
|
||||||
|
|
||||||
|
def save(self, filepath, data):
|
||||||
|
directory = os.path.dirname(filepath)
|
||||||
|
|
||||||
|
if not os.path.exists(directory):
|
||||||
|
try:
|
||||||
|
os.makedirs(directory)
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno != errno.EEXIST:
|
||||||
|
raise
|
||||||
|
|
||||||
|
if not os.path.isdir(directory):
|
||||||
|
raise IOError("{} is not a directory".format(directory))
|
||||||
|
|
||||||
|
with open(filepath, "wb") as f:
|
||||||
|
f.write(data)
|
67
lama_cleaner/file_manager/utils.py
Normal file
67
lama_cleaner/file_manager/utils.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# Copy from: https://github.com/silentsokolov/flask-thumbnails/blob/master/flask_thumbnails/utils.py
|
||||||
|
import importlib
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
|
||||||
|
def generate_filename(original_filename, *options):
|
||||||
|
name, ext = os.path.splitext(original_filename)
|
||||||
|
for v in options:
|
||||||
|
if v:
|
||||||
|
name += "_%s" % v
|
||||||
|
name += ext
|
||||||
|
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def parse_size(size):
|
||||||
|
if isinstance(size, int):
|
||||||
|
# If the size parameter is a single number, assume square aspect.
|
||||||
|
return [size, size]
|
||||||
|
|
||||||
|
if isinstance(size, (tuple, list)):
|
||||||
|
if len(size) == 1:
|
||||||
|
# If single value tuple/list is provided, exand it to two elements
|
||||||
|
return size + type(size)(size)
|
||||||
|
return size
|
||||||
|
|
||||||
|
try:
|
||||||
|
thumbnail_size = [int(x) for x in size.lower().split("x", 1)]
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError( # pylint: disable=raise-missing-from
|
||||||
|
"Bad thumbnail size format. Valid format is INTxINT."
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(thumbnail_size) == 1:
|
||||||
|
# If the size parameter only contains a single integer, assume square aspect.
|
||||||
|
thumbnail_size.append(thumbnail_size[0])
|
||||||
|
|
||||||
|
return thumbnail_size
|
||||||
|
|
||||||
|
|
||||||
|
def aspect_to_string(size):
|
||||||
|
if isinstance(size, str):
|
||||||
|
return size
|
||||||
|
|
||||||
|
return "x".join(map(str, size))
|
||||||
|
|
||||||
|
|
||||||
|
IMG_SUFFIX = {'.jpg', '.jpeg', '.png'}
|
||||||
|
|
||||||
|
|
||||||
|
def glob_img(p: Union[Path, str], recursive: bool = False):
|
||||||
|
p = Path(p)
|
||||||
|
if p.is_file() and p.suffix in IMG_SUFFIX:
|
||||||
|
yield p
|
||||||
|
else:
|
||||||
|
if recursive:
|
||||||
|
files = Path(p).glob("**/*.*")
|
||||||
|
else:
|
||||||
|
files = Path(p).glob("*.*")
|
||||||
|
|
||||||
|
for it in files:
|
||||||
|
if it.suffix not in IMG_SUFFIX:
|
||||||
|
continue
|
||||||
|
yield it
|
@ -1,6 +1,9 @@
|
|||||||
import os
|
import os
|
||||||
import imghdr
|
import imghdr
|
||||||
import argparse
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
|
||||||
def parse_args():
|
def parse_args():
|
||||||
@ -48,7 +51,12 @@ def parse_args():
|
|||||||
help="Set window size for GUI",
|
help="Set window size for GUI",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--input", type=str, help="Path to image you want to load by default"
|
"--input", type=str,
|
||||||
|
help="If input is image, it will be load by default. If input is directory, all images will be loaded to file manager"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--output-dir", type=str,
|
||||||
|
help="Only required when --input is directory. Output directory for all processed images"
|
||||||
)
|
)
|
||||||
parser.add_argument("--disable-model-switch", action="store_true", help="Disable model switch in frontend")
|
parser.add_argument("--disable-model-switch", action="store_true", help="Disable model switch in frontend")
|
||||||
parser.add_argument("--debug", action="store_true")
|
parser.add_argument("--debug", action="store_true")
|
||||||
@ -57,8 +65,20 @@ def parse_args():
|
|||||||
if args.input is not None:
|
if args.input is not None:
|
||||||
if not os.path.exists(args.input):
|
if not os.path.exists(args.input):
|
||||||
parser.error(f"invalid --input: {args.input} not exists")
|
parser.error(f"invalid --input: {args.input} not exists")
|
||||||
if imghdr.what(args.input) is None:
|
if os.path.isfile(args.input):
|
||||||
parser.error(f"invalid --input: {args.input} is not a valid image file")
|
if imghdr.what(args.input) is None:
|
||||||
|
parser.error(f"invalid --input: {args.input} is not a valid image file")
|
||||||
|
else:
|
||||||
|
if args.output_dir is None:
|
||||||
|
parser.error(f"invalid --input: {args.input} is a directory, --output-dir is required")
|
||||||
|
else:
|
||||||
|
output_dir = Path(args.output_dir)
|
||||||
|
if not output_dir.exists():
|
||||||
|
logger.info(f"Creating output directory: {output_dir}")
|
||||||
|
output_dir.mkdir(parents=True)
|
||||||
|
else:
|
||||||
|
if not output_dir.is_dir():
|
||||||
|
parser.error(f"invalid --output-dir: {output_dir} is not a directory")
|
||||||
|
|
||||||
if args.model == 'sd1.5' and not args.sd_run_local:
|
if args.model == 'sd1.5' and not args.sd_run_local:
|
||||||
if not args.hf_access_token.startswith("hf_"):
|
if not args.hf_access_token.startswith("hf_"):
|
||||||
|
@ -20,6 +20,7 @@ from loguru import logger
|
|||||||
from lama_cleaner.interactive_seg import InteractiveSeg, Click
|
from lama_cleaner.interactive_seg import InteractiveSeg, Click
|
||||||
from lama_cleaner.model_manager import ModelManager
|
from lama_cleaner.model_manager import ModelManager
|
||||||
from lama_cleaner.schema import Config
|
from lama_cleaner.schema import Config
|
||||||
|
from lama_cleaner.file_manager import FileManager
|
||||||
|
|
||||||
try:
|
try:
|
||||||
torch._C._jit_override_can_fuse_on_cpu(False)
|
torch._C._jit_override_can_fuse_on_cpu(False)
|
||||||
@ -29,7 +30,8 @@ try:
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
from flask import Flask, request, send_file, cli, make_response
|
from flask import Flask, request, send_file, cli, make_response, send_from_directory, jsonify
|
||||||
|
from flask_caching import Cache
|
||||||
|
|
||||||
# Disable ability for Flask to display warning about using a development server in a production environment.
|
# Disable ability for Flask to display warning about using a development server in a production environment.
|
||||||
# https://gist.github.com/jerblack/735b9953ba1ab6234abb43174210d356
|
# https://gist.github.com/jerblack/735b9953ba1ab6234abb43174210d356
|
||||||
@ -65,15 +67,15 @@ class NoFlaskwebgui(logging.Filter):
|
|||||||
|
|
||||||
logging.getLogger("werkzeug").addFilter(NoFlaskwebgui())
|
logging.getLogger("werkzeug").addFilter(NoFlaskwebgui())
|
||||||
|
|
||||||
|
cache = Cache(config={'CACHE_TYPE': 'SimpleCache'}, with_jinja2_ext=False)
|
||||||
|
|
||||||
app = Flask(__name__, static_folder=os.path.join(BUILD_DIR, "static"))
|
app = Flask(__name__, static_folder=os.path.join(BUILD_DIR, "static"))
|
||||||
app.config["JSON_AS_ASCII"] = False
|
app.config["JSON_AS_ASCII"] = False
|
||||||
CORS(app, expose_headers=["Content-Disposition"])
|
CORS(app, expose_headers=["Content-Disposition"])
|
||||||
# MAX_BUFFER_SIZE = 50 * 1000 * 1000 # 50 MB
|
cache.init_app(app)
|
||||||
# async_mode 优先级: eventlet/gevent_uwsgi/gevent/threading
|
|
||||||
# only threading works on macOS
|
|
||||||
# socketio = SocketIO(app, max_http_buffer_size=MAX_BUFFER_SIZE, async_mode='threading')
|
|
||||||
|
|
||||||
model: ModelManager = None
|
model: ModelManager = None
|
||||||
|
thumb = FileManager(app)
|
||||||
interactive_seg_model: InteractiveSeg = None
|
interactive_seg_model: InteractiveSeg = None
|
||||||
device = None
|
device = None
|
||||||
input_image_path: str = None
|
input_image_path: str = None
|
||||||
@ -93,6 +95,38 @@ def diffuser_callback(i, t, latents):
|
|||||||
# socketio.emit('diffusion_step', {'diffusion_step': step})
|
# socketio.emit('diffusion_step', {'diffusion_step': step})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/medias")
|
||||||
|
def medias():
|
||||||
|
# all images in input folder
|
||||||
|
return jsonify(thumb.media_names), 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/media/<filename>')
|
||||||
|
def media_file(filename):
|
||||||
|
return send_from_directory(app.config['THUMBNAIL_MEDIA_ROOT'], filename)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/media_thumbnail/<filename>')
|
||||||
|
def media_thumbnail_file(filename):
|
||||||
|
args = request.args
|
||||||
|
width = args.get('width')
|
||||||
|
height = args.get('height')
|
||||||
|
if width is None and height is None:
|
||||||
|
width = 256
|
||||||
|
if width:
|
||||||
|
width = int(float(width))
|
||||||
|
if height:
|
||||||
|
height = int(float(height))
|
||||||
|
|
||||||
|
thumb_filename, (width, height) = thumb.get_thumbnail(filename, width, height)
|
||||||
|
thumb_filepath = f"{app.config['THUMBNAIL_MEDIA_THUMBNAIL_ROOT']}{thumb_filename}"
|
||||||
|
|
||||||
|
response = make_response(send_file(thumb_filepath))
|
||||||
|
response.headers["X-Width"] = str(width)
|
||||||
|
response.headers["X-Height"] = str(height)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@app.route("/inpaint", methods=["POST"])
|
@app.route("/inpaint", methods=["POST"])
|
||||||
def process():
|
def process():
|
||||||
input = request.files
|
input = request.files
|
||||||
@ -294,12 +328,17 @@ def main(args):
|
|||||||
global is_desktop
|
global is_desktop
|
||||||
|
|
||||||
device = torch.device(args.device)
|
device = torch.device(args.device)
|
||||||
input_image_path = args.input
|
|
||||||
is_disable_model_switch = args.disable_model_switch
|
is_disable_model_switch = args.disable_model_switch
|
||||||
is_desktop = args.gui
|
is_desktop = args.gui
|
||||||
if is_disable_model_switch:
|
if is_disable_model_switch:
|
||||||
logger.info(f"Start with --disable-model-switch, model switch on frontend is disable")
|
logger.info(f"Start with --disable-model-switch, model switch on frontend is disable")
|
||||||
|
|
||||||
|
if os.path.isdir(args.input):
|
||||||
|
app.config["THUMBNAIL_MEDIA_ROOT"] = args.input
|
||||||
|
app.config["THUMBNAIL_MEDIA_THUMBNAIL_ROOT"] = os.path.join(args.output_dir, 'thumbnails')
|
||||||
|
else:
|
||||||
|
input_image_path = args.input
|
||||||
|
|
||||||
model = ModelManager(
|
model = ModelManager(
|
||||||
name=args.model,
|
name=args.model,
|
||||||
device=device,
|
device=device,
|
||||||
|
Loading…
Reference in New Issue
Block a user