Commit bcea7de2 authored by dmMaze's avatar dmMaze
Browse files

allow empty cache manually #53

parent af6d9799
Loading
Loading
Loading
Loading
+45 −1
Original line number Diff line number Diff line
@@ -40,6 +40,8 @@ class BaseModule:
    download_file_list: List = None
    download_file_on_load = False

    _load_model_keys: set = None

    def __init__(self, **params) -> None:
        if params:
            if self.params is None:
@@ -89,6 +91,40 @@ class BaseModule:
            return True
        return False
    
    def unload_model(self, empty_cache=False):
        model_deleted = False
        if self._load_model_keys is not None:
            for k in self._load_model_keys:
                model = getattr(self, k)
                if model is not None:
                    del model
                    setattr(self, k, None)
                    model_deleted = True
    
        if empty_cache and model_deleted:
            soft_empty_cache()

        return model_deleted

    def load_model(self):
        # TODO: check and download files
        self._load_model()
        return

    def _load_model(self):
        return

    def all_model_loaded(self):
        if self._load_model_keys is None:
            return True
        for k in self._load_model_keys:
            if not hasattr(self, k) or getattr(self, k) is None:
                return False
        return True
    
    def __del__(self):
        self.unload_model()

os.environ['PYTORCH_ENABLE_MPS_FALLBACK'] = '1'
import torch

@@ -97,8 +133,15 @@ if hasattr(torch, 'cuda') and torch.cuda.is_available():
    DEFAULT_DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
elif hasattr(torch, 'backends') and hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():
    DEFAULT_DEVICE = 'mps'
BF16_SUPPORTED = DEFAULT_DEVICE == 'cuda' and torch.cuda.is_bf16_supported()

def gc_collect():
def is_nvidia():
    if DEFAULT_DEVICE == 'cuda':
        if torch.version.cuda:
            return True
    return False

def soft_empty_cache():
    gc.collect()
    if DEFAULT_DEVICE == 'cuda':
        torch.cuda.empty_cache()
@@ -123,3 +166,4 @@ TORCH_DTYPE_MAP = {
    'fp16': torch.float16,
    'bf16': torch.bfloat16,
}
    
 No newline at end of file
+37 −42
Original line number Diff line number Diff line
@@ -8,7 +8,7 @@ from utils.registry import Registry
from utils.textblock_mask import extract_ballon_mask
from utils.imgproc_utils import enlarge_window

from ..base import BaseModule, DEFAULT_DEVICE, gc_collect, DEVICE_SELECTOR, GPUINTENSIVE_SET, TORCH_DTYPE_MAP
from ..base import BaseModule, DEFAULT_DEVICE, soft_empty_cache, DEVICE_SELECTOR, GPUINTENSIVE_SET, TORCH_DTYPE_MAP, BF16_SUPPORTED
from ..textdetector import TextBlock

INPAINTERS = Registry('inpainters')
@@ -30,10 +30,6 @@ class InpainterBase(BaseModule):
            if INPAINTERS.module_dict[key] == self.__class__:
                self.name = key
                break
        self.setup_inpainter()

    def setup_inpainter(self):
        raise NotImplementedError
    
    def memory_safe_inpaint(self, img: np.ndarray, mask: np.ndarray, textblock_list: List[TextBlock] = None) -> np.ndarray:
        '''
@@ -43,7 +39,7 @@ class InpainterBase(BaseModule):
            return self._inpaint(img, mask, textblock_list)
        except Exception as e:
            if DEFAULT_DEVICE == 'cuda' and isinstance(e, torch.cuda.OutOfMemoryError):
                gc_collect()
                soft_empty_cache()
                try:
                    return self._inpaint(img, mask, textblock_list)
                except Exception as ee:
@@ -62,6 +58,10 @@ class InpainterBase(BaseModule):
                raise e

    def inpaint(self, img: np.ndarray, mask: np.ndarray, textblock_list: List[TextBlock] = None, check_need_inpaint: bool = False) -> np.ndarray:
        
        if not self.all_model_loaded():
            self.load_model()
        
        if not self.inpaint_by_block or textblock_list is None:
            if check_need_inpaint:
                ballon_msk, non_text_msk = extract_ballon_mask(img, mask)
@@ -119,9 +119,11 @@ class InpainterBase(BaseModule):
@register_inpainter('opencv-tela')
class OpenCVInpainter(InpainterBase):

    def setup_inpainter(self):
    def __init__(self, **params) -> None:
        super().__init__(**params)
        self.inpaint_method = lambda img, mask, *args, **kwargs: cv2.inpaint(img, mask, 3, cv2.INPAINT_NS)
        
    
    def _inpaint(self, img: np.ndarray, mask: np.ndarray, textblock_list: List[TextBlock] = None) -> np.ndarray:
        return self.inpaint_method(img, mask)

@@ -154,7 +156,8 @@ class PatchmatchInpainter(InpainterBase):
                'archive_sha256_pre_calculated': 'c991ff61f7cb3efaf8e75d957e62d56ba646083bc25535f913ac65775c16ca65'
        }]

    def setup_inpainter(self):
    def __init__(self, **params) -> None:
        super().__init__(**params)
        from . import patch_match
        self.inpaint_method = lambda img, mask, *args, **kwargs: patch_match.inpaint(img, mask, patch_size=3)
    
@@ -171,8 +174,6 @@ class PatchmatchInpainter(InpainterBase):
import torch
from utils.imgproc_utils import resize_keepasp
from .aot import AOTGenerator, load_aot_model
AOTMODEL: AOTGenerator = None
AOTMODEL_PATH = 'data/models/aot_inpainter.ckpt'


@register_inpainter('aot')
@@ -194,6 +195,7 @@ class AOTInpainter(InpainterBase):
    device = DEFAULT_DEVICE
    inpaint_size = 2048
    model: AOTGenerator = None
    _load_model_keys = {'model'}

    download_file_list = [{
            'url': 'https://github.com/zyddnys/manga-image-translator/releases/download/beta-0.3/inpainting.ckpt',
@@ -201,15 +203,15 @@ class AOTInpainter(InpainterBase):
            'files': 'data/models/aot_inpainter.ckpt',
    }]

    def setup_inpainter(self):
        global AOTMODEL
    def __init__(self, **params) -> None:
        super().__init__(**params)
        self.device = self.params['device']['select']
        if AOTMODEL is None:
            self.model = AOTMODEL = load_aot_model(AOTMODEL_PATH, self.device)
        else:
            self.model = AOTMODEL
            self.model.to(self.device)
        self.inpaint_size = int(self.params['inpaint_size']['select'])
        self.model: AOTGenerator = None
        
    def _load_model(self):
        AOTMODEL_PATH = 'data/models/aot_inpainter.ckpt'
        self.model = load_aot_model(AOTMODEL_PATH, self.device)

    def moveToDevice(self, device: str, precision: str = None):
        self.model.to(device)
@@ -274,10 +276,13 @@ class AOTInpainter(InpainterBase):
        elif param_key == 'inpaint_size':
            self.inpaint_size = int(self.params['inpaint_size']['select'])

    def unload_model(self):
        del self.model
        super().unload_model(True)


from .lama import LamaFourier, load_lama_mpe

LAMA_MPE: LamaFourier = None
@register_inpainter('lama_mpe')
class LamaInpainterMPE(InpainterBase):

@@ -292,27 +297,23 @@ class LamaInpainterMPE(InpainterBase):
        }, 
        'device': DEVICE_SELECTOR()
    }
    precision = 'fp32'

    device = DEFAULT_DEVICE
    inpaint_size = 2048

    download_file_list = [{
            'url': 'https://github.com/zyddnys/manga-image-translator/releases/download/beta-0.3/inpainting_lama_mpe.ckpt',
            'sha256_pre_calculated': 'd625aa1b3e0d0408acfd6928aa84f005867aa8dbb9162480346a4e20660786cc',
            'files': 'data/models/lama_mpe.ckpt',
    }]
    _load_model_keys = {'model'}

    def setup_inpainter(self):
        global LAMA_MPE

    def __init__(self, **params) -> None:
        super().__init__(**params)
        self.device = self.params['device']['select']
        if LAMA_MPE is None:
            self.model = LAMA_MPE = load_lama_mpe(r'data/models/lama_mpe.ckpt', self.device)
        else:
            self.model = LAMA_MPE
            self.model.to(self.device)
        self.inpaint_size = int(self.params['inpaint_size']['select'])
        self.precision = 'fp32'
        self.model: LamaFourier = None

    def _load_model(self):
        self.model = load_lama_mpe(r'data/models/lama_mpe.ckpt', self.device)

    def inpaint_preprocess(self, img: np.ndarray, mask: np.ndarray) -> np.ndarray:

@@ -401,7 +402,6 @@ class LamaInpainterMPE(InpainterBase):
        if precision is not None:
            self.precision = precision

LAMA_LARGE: LamaFourier = None
@register_inpainter('lama_large_512px')
class LamaLarge(LamaInpainterMPE):

@@ -424,7 +424,7 @@ class LamaLarge(LamaInpainterMPE):
                'fp32',
                'bf16'
            ], 
            'select': 'bf16' if DEFAULT_DEVICE == 'cuda' else 'fp32'
            'select': 'bf16' if BF16_SUPPORTED == 'cuda' else 'fp32'
        }, 
    }

@@ -434,20 +434,15 @@ class LamaLarge(LamaInpainterMPE):
            'files': 'data/models/lama_large_512px.ckpt',
    }]

    device = DEFAULT_DEVICE
    inpaint_size = 1024

    def setup_inpainter(self):
        global LAMA_LARGE
    def __init__(self, **params) -> None:
        super().__init__(**params)
        self.precision = self.params['precision']['select']

    def _load_model(self):
        device = self.params['device']['select']
        self.inpaint_size = int(self.params['inpaint_size']['select'])
        precision = self.params['precision']['select']

        if LAMA_LARGE is None:
            self.model = LAMA_LARGE = load_lama_mpe(r'data/models/lama_large_512px.ckpt', device='cpu', use_mpe=False, large_arch=True)
        else:
            self.model = LAMA_LARGE
        self.model = load_lama_mpe(r'data/models/lama_large_512px.ckpt', device='cpu', use_mpe=False, large_arch=True)
        self.moveToDevice(device, precision=precision)


+41 −65
Original line number Diff line number Diff line
@@ -24,12 +24,12 @@ class OCRBase(BaseModule):
            if OCR.module_dict[key] == self.__class__:
                self.name = key
                break
        self.setup_ocr()

    def setup_ocr(self):
        raise NotImplementedError

    def run_ocr(self, img: np.ndarray, blk_list: List[TextBlock] = None) -> Union[List[TextBlock], str]:

        if not self.all_model_loaded():
            self.load_model()

        if blk_list is None:
            text = self.ocr_img(img)
            return text
@@ -38,7 +38,7 @@ class OCRBase(BaseModule):

        for blk in blk_list:
            blk.text = []
        self.ocr_blk_list(img, blk_list)
        self._ocr_blk_list(img, blk_list)
        for callback_name, callback in self._postprocess_hooks.items():
            callback(textblocks=blk_list, img=img, ocr_module=self)
        # for blk in blk_list:
@@ -51,7 +51,7 @@ class OCRBase(BaseModule):
        #             blk.text = callback(blk.text, blk=blk)
        return blk_list

    def ocr_blk_list(self, img: np.ndarray, blk_list: List[TextBlock]) -> None:
    def _ocr_blk_list(self, img: np.ndarray, blk_list: List[TextBlock]) -> None:
        raise NotImplementedError

    def ocr_img(self, img: np.ndarray) -> str:
@@ -59,9 +59,6 @@ class OCRBase(BaseModule):


from .model_32px import OCR32pxModel
OCR32PXMODEL: OCR32pxModel = None
OCR32PXMODEL_PATH = r'data/models/mit32px_ocr.ckpt'

@register_OCR('mit32px')
class OCRMIT32px(OCRBase):
    params = {
@@ -84,24 +81,21 @@ class OCRMIT32px(OCRBase):
        'archived_files': 'ocr.zip',
        'archive_sha256_pre_calculated': '47405638b96fa2540a5ee841a4cd792f25062c09d9458a973362d40785f95d7a',
    }]
    _load_model_keys = {'model'}

    def setup_ocr(self):
        
        global OCR32PXMODEL
    def __init__(self, **params) -> None:
        super().__init__(**params)
        self.device = self.params['device']['select']
        self.chunk_size = int(self.params['chunk_size']['select'])
        if OCR32PXMODEL is None:
            self.model = OCR32PXMODEL = \
                OCR32pxModel(OCR32PXMODEL_PATH, self.device, self.chunk_size)
        else:
            self.model = OCR32PXMODEL
            self.model.to(self.device)
            self.model.max_chunk_size = self.chunk_size
        self.model: OCR32pxModel = None

    def _load_model(self):
        self.model = OCR32pxModel(r'data/models/mit32px_ocr.ckpt', self.device, self.chunk_size)

    def ocr_img(self, img: np.ndarray) -> str:
        return self.model.ocr_img(img)

    def ocr_blk_list(self, img: np.ndarray, blk_list: List[TextBlock]):
    def _ocr_blk_list(self, img: np.ndarray, blk_list: List[TextBlock]):
        return self.model(img, blk_list)

    def updateParam(self, param_key: str, param_content):
@@ -115,8 +109,6 @@ class OCRMIT32px(OCRBase):




MANGA_OCR_MODEL = None
@register_OCR('manga_ocr')
class MangaOCR(OCRBase):
    params = {
@@ -131,26 +123,21 @@ class MangaOCR(OCRBase):
        'save_dir': 'data/models/manga-ocr-base',
        'concatenate_url_filename': 1,
    }]
    _load_model_keys = {'model'}

    def setup_ocr(self):
    def __init__(self, **params) -> None:
        super().__init__(**params)
        self.device = self.params['device']['select']
        self.model: MangaOCR = None

    def _load_model(self):
        from .manga_ocr import MangaOcr
        def load_manga_ocr(device='cpu') -> MangaOcr:
            manga_ocr = MangaOcr(device=device)
            return manga_ocr
        
        global MANGA_OCR_MODEL
        self.device = self.params['device']['select']
        if MANGA_OCR_MODEL is None:
            self.model = MANGA_OCR_MODEL = load_manga_ocr(self.device)
        else:
            self.model = MANGA_OCR_MODEL
            self.model.to(self.device)
        self.model = MangaOcr(device=self.device)

    def ocr_img(self, img: np.ndarray) -> str:
        return self.model(img)

    def ocr_blk_list(self, img: np.ndarray, blk_list: List[TextBlock]):
    def _ocr_blk_list(self, img: np.ndarray, blk_list: List[TextBlock]):
        im_h, im_w = img.shape[:2]
        for blk in blk_list:
            x1, y1, x2, y2 = blk.xyxy
@@ -169,11 +156,7 @@ class MangaOCR(OCRBase):




from .mit48px_ctc import OCR48pxCTC
OCR48PXCTCMODEL: OCR48pxCTC = None
OCR48PXCTCMODEL_PATH = r'data/models/mit48pxctc_ocr.ckpt'

@register_OCR('mit48px_ctc')
class OCRMIT48pxCTC(OCRBase):
    params = {
@@ -196,24 +179,21 @@ class OCRMIT48pxCTC(OCRBase):
        'archived_files': 'ocr-ctc.zip',
        'archive_sha256_pre_calculated': 'fc61c52f7a811bc72c54f6be85df814c6b60f63585175db27cb94a08e0c30101',
    }]
    _load_model_keys = {'model'}

    def setup_ocr(self):
        
        global OCR48PXCTCMODEL
    def __init__(self, **params) -> None:
        super().__init__(**params)
        self.device = self.params['device']['select']
        self.chunk_size = int(self.params['chunk_size']['select'])
        if OCR48PXCTCMODEL is None:
            self.model = OCR48PXCTCMODEL = \
                OCR48pxCTC(OCR48PXCTCMODEL_PATH, self.device, self.chunk_size)
        else:
            self.model = OCR48PXCTCMODEL
            self.model.to(self.device)
            self.model.max_chunk_size = self.chunk_size
        self.model: OCR48pxCTC = None

    def _load_model(self):
        self.model = OCR48pxCTC(r'data/models/mit48pxctc_ocr.ckpt', self.device, self.chunk_size)

    def ocr_img(self, img: np.ndarray) -> str:
        return self.model.ocr_img(img)

    def ocr_blk_list(self, img: np.ndarray, blk_list: List[TextBlock]):
    def _ocr_blk_list(self, img: np.ndarray, blk_list: List[TextBlock]):
        return self.model(img, blk_list)

    def updateParam(self, param_key: str, param_content):
@@ -228,9 +208,7 @@ class OCRMIT48pxCTC(OCRBase):


from .mit48px import Model48pxOCR
OCR48PXMODEL: Model48pxOCR = None
OCR48PXMODEL_PATH = r'data/models/ocr_ar_48px.ckpt'

@register_OCR('mit48px')
class OCRMIT48px(OCRBase):
    params = {
@@ -245,19 +223,17 @@ class OCRMIT48px(OCRBase):
        'sha256_pre_calculated': ['29daa46d080818bb4ab239a518a88338cbccff8f901bef8c9db191a7cb97671d', None],
        'concatenate_url_filename': 2,
    }]
    _load_model_keys = {'model'}

    def setup_ocr(self):
        
        global OCR48PXMODEL
    def __init__(self, **params) -> None:
        super().__init__(**params)
        self.device = self.params['device']['select']
        if OCR48PXMODEL is None:
            self.model = OCR48PXMODEL = \
                Model48pxOCR(OCR48PXMODEL_PATH, self.device)
        else:
            self.model = OCR48PXMODEL
            self.model.to(self.device)
        self.model: Model48pxOCR = None

    def _load_model(self):
        self.model = Model48pxOCR(OCR48PXMODEL_PATH, self.device)

    def ocr_blk_list(self, img: np.ndarray, blk_list: List[TextBlock]):
    def _ocr_blk_list(self, img: np.ndarray, blk_list: List[TextBlock]):
        return self.model(img, blk_list)

    def updateParam(self, param_key: str, param_content):
@@ -312,7 +288,7 @@ if platform.mac_ver()[0] >= '10.15':
            def ocr_img(self, img: np.ndarray) -> str:
                return self.model(img)

            def ocr_blk_list(self, img: np.ndarray, blk_list: List[TextBlock]):
            def _ocr_blk_list(self, img: np.ndarray, blk_list: List[TextBlock]):
                im_h, im_w = img.shape[:2]
                for blk in blk_list:
                    x1, y1, x2, y2 = blk.xyxy
@@ -374,7 +350,7 @@ if platform.system() == 'Windows' and platform.version() >= '10.0.10240.0':
            def ocr_img(self, img: np.ndarray) -> str:
                self.engine(img)

            def ocr_blk_list(self, img: np.ndarray, blk_list: List[TextBlock]) -> None:
            def _ocr_blk_list(self, img: np.ndarray, blk_list: List[TextBlock]) -> None:
                im_h, im_w = img.shape[:2]
                for blk in blk_list:
                    x1, y1, x2, y2 = blk.xyxy
+16 −19
Original line number Diff line number Diff line
import numpy as np
import cv2
from typing import Dict, List, Tuple
from typing import Union, List, Tuple
from collections import OrderedDict

from utils.textblock import TextBlock
@@ -30,17 +30,16 @@ class TextDetectorBase(BaseModule):
            if TEXTDETECTORS.module_dict[key] == self.__class__:
                self.name = key
                break
        self.setup_detector()

    def setup_detector(self):
        raise NotImplementedError

    def detect(self, img: np.ndarray) -> Tuple[np.ndarray, List[TextBlock]]:
        raise NotImplementedError
        if not self.all_model_loaded():
            self.load_model()
        return self._detect(img)

from .ctd import CTDModel
CTDMODEL_TORCH: CTDModel = None # if cuda is available
CTDMODEL_ONNX: CTDModel = None  # for cpu inference
CTD_ONNX_PATH = 'data/models/comictextdetector.pt.onnx'
CTD_TORCH_PATH = 'data/models/comictextdetector.pt'

@@ -66,27 +65,25 @@ class ComicTextDetector(TextDetectorBase):
        'device': DEVICE_SELECTOR(),
        'description': 'ComicTextDetector'
    }
    _load_model_keys = {'model'}

    device = DEFAULT_DEVICE
    detect_size = 1280
    def setup_detector(self):
        global CTDMODEL_TORCH, CTDMODEL_ONNX
    def __init__(self, **params) -> None:
        super().__init__(**params)

        self.device = self.params['device']['select']
        self.detect_size = int(self.params['detect_size']['select'])
        self.model: CTDModel = None

    def _load_model(self):
        if self.device != 'cpu':
            if CTDMODEL_TORCH is None:
                self.detector = CTDMODEL_TORCH = load_ctd_model(CTD_TORCH_PATH, self.device, self.detect_size)
            else:
                self.detector = CTDMODEL_TORCH
        else:
            if CTDMODEL_ONNX is None:
                self.detector = CTDMODEL_ONNX = load_ctd_model(CTD_ONNX_PATH, self.device, self.detect_size)
            self.model = load_ctd_model(CTD_TORCH_PATH, self.device, self.detect_size)
        else:
                self.detector = CTDMODEL_ONNX

    def detect(self, img: np.ndarray) -> Tuple[np.ndarray, List[TextBlock]]:
            self.model = load_ctd_model(CTD_ONNX_PATH, self.device, self.detect_size)

        _, mask, blk_list = self.detector(img)
    def _detect(self, img: np.ndarray) -> Tuple[np.ndarray, List[TextBlock]]:
        _, mask, blk_list = self.model(img)
        return mask, blk_list

    def updateParam(self, param_key: str, param_content):
@@ -96,4 +93,4 @@ class ComicTextDetector(TextDetectorBase):
        if device != self.device:
            self.setup_detector()
        elif detect_size != self.detect_size:
            self.detector.detect_size = detect_size
 No newline at end of file
            self.model.detect_size = detect_size
 No newline at end of file
+46 −19
Original line number Diff line number Diff line
from typing import List, Union, Tuple

from qtpy.QtWidgets import QKeySequenceEdit, QLayout, QGridLayout, QHBoxLayout, QVBoxLayout, QTreeView, QWidget, QLabel, QSizePolicy, QSpacerItem, QCheckBox, QSplitter, QScrollArea, QGroupBox, QLineEdit
from qtpy.QtWidgets import QPushButton, QKeySequenceEdit, QLayout, QGridLayout, QHBoxLayout, QVBoxLayout, QTreeView, QWidget, QLabel, QSizePolicy, QSpacerItem, QCheckBox, QSplitter, QScrollArea, QGroupBox, QLineEdit
from qtpy.QtCore import Qt, Signal, QSize, QEvent, QItemSelection
from qtpy.QtGui import QStandardItem, QStandardItemModel, QMouseEvent, QFont, QColor, QPalette, QIntValidator, QValidator, QFocusEvent
from qtpy import API
@@ -146,6 +146,25 @@ def combobox_with_label(sel: List[str], name: str, discription: str = None, vert
        layout.addWidget(combox)
        return combox, target_block
    
def checkbox_with_label(name: str, discription: str = None, target_block: QWidget = None):
    checkbox = QCheckBox()
    if discription is not None:
        font = checkbox.font()
        font.setPointSizeF(CONFIG_FONTSIZE_CONTENT * 0.8)
        checkbox.setFont(font)
        checkbox.setText(discription)
        vertical_layout = True
    else:
        vertical_layout = False

    if target_block is None:
        sublock = ConfigSubBlock(checkbox, name, vertical_layout=vertical_layout)
        if vertical_layout is False:
            sublock.layout().addItem(QSpacerItem(0, 0, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding))
        target_block = sublock
    return checkbox, target_block
    


class ConfigBlock(Widget):
    sublock_pressed = Signal(int, int)
@@ -196,23 +215,10 @@ class ConfigBlock(Widget):
        self.addSublock(sublock)
        return sublock

    def addCheckBox(self, name: str, discription: str = None, sublock: ConfigSubBlock = None) -> QCheckBox:
        checkbox = QCheckBox()
        if discription is not None:
            font = checkbox.font()
            font.setPointSizeF(CONFIG_FONTSIZE_CONTENT * 0.8)
            checkbox.setFont(font)
            checkbox.setText(discription)
            vertical_layout = True
        else:
            vertical_layout = False
        if sublock is None:
            sublock = ConfigSubBlock(checkbox, name, vertical_layout=vertical_layout)
            if vertical_layout is False:
                sublock.layout().addItem(QSpacerItem(0, 0, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding))
    def addCheckBox(self, name: str, discription: str = None, target_block: ConfigSubBlock = None) -> QCheckBox:
        checkbox, sublock = checkbox_with_label(name, discription, target_block)
        if target_block is None:
            self.addSublock(sublock)
        else:
            sublock.layout().addWidget(checkbox)
        return checkbox, sublock

    def getSubBlockbyIdx(self, idx: int) -> ConfigSubBlock:
@@ -344,6 +350,7 @@ class ConfigTable(QTreeView):
class ConfigPanel(Widget):

    save_config = Signal()
    unload_models = Signal()

    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
@@ -376,6 +383,18 @@ class ConfigPanel(Widget):
            TableItem(label_saladict, CONFIG_FONTSIZE_TABLE),
        ])
        
        self.load_model_checker, msublock = checkbox_with_label(self.tr('Load models on demand'), discription=self.tr('Load models on demand to save memory.'))
        self.load_model_checker.stateChanged.connect(self.on_load_model_changed)
        dlConfigPanel.vlayout.addWidget(msublock)
        self.empty_runcache_checker, msublock = checkbox_with_label(self.tr('Empty cache after RUN'), discription=self.tr('Empty cache after RUN to save memory.'))
        dlConfigPanel.vlayout.addWidget(msublock)
        self.empty_runcache_checker.stateChanged.connect(self.on_runcache_changed)
        self.unload_model_btn = QPushButton(parent=self)
        self.unload_model_btn.setFixedWidth(500)
        self.unload_model_btn.setText(self.tr('Unload All Models'))
        self.unload_model_btn.clicked.connect(self.unload_models)
        msublock.layout().addWidget(self.unload_model_btn)

        dlConfigPanel.addTextLabel(label_text_det)
        self.detect_config_panel = TextDetectConfigPanel(self.tr('Detector'), scrollWidget=self)
        self.detect_sub_block = dlConfigPanel.addBlockWidget(self.detect_config_panel)
@@ -437,7 +456,7 @@ class ConfigPanel(Widget):

        self.let_autolayout_checker, sublock = generalConfigPanel.addCheckBox(self.tr('Auto layout'), 
                discription=self.tr('Split translation into multi-lines according to the extracted balloon region.'))
        self.let_autolayout_adaptive_fntsize_checker, _ = generalConfigPanel.addCheckBox(None, self.tr('Adjust font size adaptively if it is set to \"decide by program.\"'), sublock=sublock)
        self.let_autolayout_adaptive_fntsize_checker, _ = generalConfigPanel.addCheckBox(None, self.tr('Adjust font size adaptively if it is set to \"decide by program.\"'), target_block=sublock)
        self.let_autolayout_adaptive_fntsize_checker.stateChanged.connect(self.on_adaptive_fntsize_changed)

        self.let_autolayout_checker.stateChanged.connect(self.on_autolayout_changed)
@@ -488,8 +507,14 @@ class ConfigPanel(Widget):

        self.configTable.expandAll()

    def on_load_model_changed(self):
        pcfg.module.load_model_on_demand = self.load_model_checker.isChecked()

    def on_runcache_changed(self):
        pcfg.module.empty_runcache = self.empty_runcache_checker.isChecked()

    def addConfigBlock(self, header: str) -> Tuple[ConfigBlock, TableItem]:
        cb = ConfigBlock(header)
        cb = ConfigBlock(header, parent=self)
        cb.sublock_pressed.connect(self.onSublockPressed)
        self.configContent.addConfigBlock(cb)
        cb.setIndex(len(self.configContent.config_block_list)-1)
@@ -589,5 +614,7 @@ class ConfigPanel(Widget):
        self.ocr_config_panel.restoreEmptyOCRChecker.setChecked(pcfg.restore_ocr_empty)
        self.rst_imgformat_combobox.setCurrentText(pcfg.imgsave_ext.replace('.', '').upper())
        self.rst_imgquality_edit.setText(str(pcfg.imgsave_quality))
        self.load_model_checker.setChecked(pcfg.module.load_model_on_demand)
        self.empty_runcache_checker.setChecked(pcfg.module.empty_runcache)

        self.blockSignals(False)
 No newline at end of file
Loading