scripts.py 11.3 KB
Newer Older
1 2 3
import os
import sys
import traceback
4
from collections import namedtuple
5

A
AUTOMATIC 已提交
6
import modules.ui as ui
7 8
import gradio as gr

A
AUTOMATIC 已提交
9
from modules.processing import StableDiffusionProcessing
10 11 12 13
from modules import shared, paths, script_callbacks

AlwaysVisible = object()

A
AUTOMATIC 已提交
14

15 16
class Script:
    filename = None
A
AUTOMATIC 已提交
17 18
    args_from = None
    args_to = None
19 20 21 22 23 24
    alwayson = False

    infotext_fields = None
    """if set in ui(), this is a list of pairs of gradio component + text; the text will be used when
    parsing infotext to set the value for the component; see ui.py's txt2img_paste_fields for an example
    """
25 26

    def title(self):
27 28
        """this function should return the title of the script. This is what will be displayed in the dropdown menu."""

29 30
        raise NotImplementedError()

A
AUTOMATIC 已提交
31
    def ui(self, is_img2img):
32 33 34 35 36
        """this function should create gradio UI elements. See https://gradio.app/docs/#components
        The return value should be an array of all components that are used in processing.
        Values of those returned componenbts will be passed to run() and process() functions.
        """

A
AUTOMATIC 已提交
37 38
        pass

A
AUTOMATIC 已提交
39
    def show(self, is_img2img):
40 41 42 43 44 45 46 47 48
        """
        is_img2img is True if this function is called for the img2img interface, and Fasle otherwise

        This function should return:
         - False if the script should not be shown in UI at all
         - True if the script should be shown in UI if it's scelected in the scripts drowpdown
         - script.AlwaysVisible if the script should be shown in UI at all times
         """

A
AUTOMATIC 已提交
49 50
        return True

51 52 53 54 55 56 57 58 59 60 61
    def run(self, p, *args):
        """
        This function is called if the script has been selected in the script dropdown.
        It must do all processing and return the Processed object with results, same as
        one returned by processing.process_images.

        Usually the processing is done by calling the processing.process_images function.

        args contains all values returned by components from ui()
        """

A
AUTOMATIC 已提交
62 63
        raise NotImplementedError()

64 65 66 67 68 69 70 71
    def process(self, p, *args):
        """
        This function is called before processing begins for AlwaysVisible scripts.
        scripts. You can modify the processing object (p) here, inject hooks, etc.
        """

        pass

A
AUTOMATIC 已提交
72
    def describe(self):
73
        """unused"""
A
AUTOMATIC 已提交
74 75
        return ""

76

77 78 79 80 81 82 83 84 85 86 87
current_basedir = paths.script_path


def basedir():
    """returns the base directory for the current script. For scripts in the main scripts directory,
    this is the main directory (where webui.py resides), and for scripts in extensions directory
    (ie extensions/aesthetic/script/aesthetic.py), this is extension's directory (extensions/aesthetic)
    """
    return current_basedir


A
AUTOMATIC 已提交
88
scripts_data = []
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
ScriptFile = namedtuple("ScriptFile", ["basedir", "filename", "path"])
ScriptClassData = namedtuple("ScriptClassData", ["script_class", "path", "basedir"])


def list_scripts(scriptdirname, extension):
    scripts_list = []

    basedir = os.path.join(paths.script_path, scriptdirname)
    if os.path.exists(basedir):
        for filename in sorted(os.listdir(basedir)):
            scripts_list.append(ScriptFile(paths.script_path, filename, os.path.join(basedir, filename)))

    extdir = os.path.join(paths.script_path, "extensions")
    if os.path.exists(extdir):
        for dirname in sorted(os.listdir(extdir)):
            dirpath = os.path.join(extdir, dirname)
A
AUTOMATIC 已提交
105 106 107
            scriptdirpath = os.path.join(dirpath, scriptdirname)

            if not os.path.isdir(scriptdirpath):
108
                continue
109

A
AUTOMATIC 已提交
110 111
            for filename in sorted(os.listdir(scriptdirpath)):
                scripts_list.append(ScriptFile(dirpath, filename, os.path.join(scriptdirpath, filename)))
112

113
    scripts_list = [x for x in scripts_list if os.path.splitext(x.path)[1].lower() == extension and os.path.isfile(x.path)]
114

115
    return scripts_list
116

117

A
AUTOMATIC 已提交
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
def list_files_with_name(filename):
    res = []

    dirs = [paths.script_path]

    extdir = os.path.join(paths.script_path, "extensions")
    if os.path.exists(extdir):
        dirs += [os.path.join(extdir, d) for d in sorted(os.listdir(extdir))]

    for dirpath in dirs:
        if not os.path.isdir(dirpath):
            continue

        path = os.path.join(dirpath, filename)
        if os.path.isfile(filename):
            res.append(path)

    return res


138 139 140 141 142 143 144 145
def load_scripts():
    global current_basedir
    scripts_data.clear()
    script_callbacks.clear_callbacks()

    scripts_list = list_scripts("scripts", ".py")

    syspath = sys.path
146

147
    for scriptfile in sorted(scripts_list):
148
        try:
149 150 151 152 153
            if scriptfile.basedir != paths.script_path:
                sys.path = [scriptfile.basedir] + sys.path
            current_basedir = scriptfile.basedir

            with open(scriptfile.path, "r", encoding="utf8") as file:
154 155
                text = file.read()

156
            from types import ModuleType
157 158
            compiled = compile(text, scriptfile.path, 'exec')
            module = ModuleType(scriptfile.filename)
159 160 161 162
            exec(compiled, module.__dict__)

            for key, script_class in module.__dict__.items():
                if type(script_class) == type and issubclass(script_class, Script):
163
                    scripts_data.append(ScriptClassData(script_class, scriptfile.path, scriptfile.basedir))
164 165

        except Exception:
166
            print(f"Error loading script: {scriptfile.filename}", file=sys.stderr)
167
            print(traceback.format_exc(), file=sys.stderr)
168

169 170 171 172
        finally:
            sys.path = syspath
            current_basedir = paths.script_path

173 174 175

def wrap_call(func, filename, funcname, *args, default=None, **kwargs):
    try:
A
AUTOMATIC 已提交
176
        res = func(*args, **kwargs)
177 178
        return res
    except Exception:
A
AUTOMATIC 已提交
179
        print(f"Error calling: {filename}/{funcname}", file=sys.stderr)
180 181 182 183 184
        print(traceback.format_exc(), file=sys.stderr)

    return default


A
AUTOMATIC 已提交
185 186 187
class ScriptRunner:
    def __init__(self):
        self.scripts = []
188 189
        self.selectable_scripts = []
        self.alwayson_scripts = []
ふぁ 已提交
190
        self.titles = []
191
        self.infotext_fields = []
A
AUTOMATIC 已提交
192 193

    def setup_ui(self, is_img2img):
194
        for script_class, path, basedir in scripts_data:
A
AUTOMATIC 已提交
195 196 197
            script = script_class()
            script.filename = path

198
            visibility = script.show(is_img2img)
A
AUTOMATIC 已提交
199

200 201 202 203
            if visibility == AlwaysVisible:
                self.scripts.append(script)
                self.alwayson_scripts.append(script)
                script.alwayson = True
A
AUTOMATIC 已提交
204

205 206 207
            elif visibility:
                self.scripts.append(script)
                self.selectable_scripts.append(script)
A
AUTOMATIC 已提交
208

209 210 211 212
        self.titles = [wrap_call(script.title, script.filename, "title") or f"{script.filename} [error]" for script in self.selectable_scripts]

        inputs = [None]
        inputs_alwayson = [True]
A
AUTOMATIC 已提交
213

214
        def create_script_ui(script, inputs, inputs_alwayson):
A
AUTOMATIC 已提交
215
            script.args_from = len(inputs)
O
OWKenobi 已提交
216
            script.args_to = len(inputs)
A
AUTOMATIC 已提交
217 218 219 220

            controls = wrap_call(script.ui, script.filename, "ui", is_img2img)

            if controls is None:
221
                return
A
AUTOMATIC 已提交
222

A
AUTOMATIC 已提交
223
            for control in controls:
D
DepFA 已提交
224
                control.custom_script_source = os.path.basename(script.filename)
225 226 227 228 229
                if not script.alwayson:
                    control.visible = False

            if script.infotext_fields is not None:
                self.infotext_fields += script.infotext_fields
A
AUTOMATIC 已提交
230

A
AUTOMATIC 已提交
231
            inputs += controls
232
            inputs_alwayson += [script.alwayson for _ in controls]
A
AUTOMATIC 已提交
233
            script.args_to = len(inputs)
A
AUTOMATIC 已提交
234

235 236 237 238
        for script in self.alwayson_scripts:
            with gr.Group():
                create_script_ui(script, inputs, inputs_alwayson)

X
xmodar 已提交
239
        dropdown = gr.Dropdown(label="Script", elem_id="script_list", choices=["None"] + self.titles, value="None", type="index")
240 241 242 243 244 245
        dropdown.save_to_config = True
        inputs[0] = dropdown

        for script in self.selectable_scripts:
            create_script_ui(script, inputs, inputs_alwayson)

A
AUTOMATIC 已提交
246
        def select_script(script_index):
247 248
            if 0 < script_index <= len(self.selectable_scripts):
                script = self.selectable_scripts[script_index-1]
A
AUTOMATIC 已提交
249 250 251 252 253
                args_from = script.args_from
                args_to = script.args_to
            else:
                args_from = 0
                args_to = 0
A
AUTOMATIC 已提交
254

255
            return [ui.gr_show(True if i == 0 else args_from <= i < args_to or is_alwayson) for i, is_alwayson in enumerate(inputs_alwayson)]
A
AUTOMATIC 已提交
256

ふぁ 已提交
257
        def init_field(title):
ふぁ 已提交
258
            if title == 'None':
ふぁ 已提交
259 260
                return
            script_index = self.titles.index(title)
261
            script = self.selectable_scripts[script_index]
ふぁ 已提交
262 263 264 265
            for i in range(script.args_from, script.args_to):
                inputs[i].visible = True

        dropdown.init_field = init_field
A
AUTOMATIC 已提交
266 267 268 269 270
        dropdown.change(
            fn=select_script,
            inputs=[dropdown],
            outputs=inputs
        )
A
AUTOMATIC 已提交
271

A
AUTOMATIC 已提交
272
        return inputs
A
AUTOMATIC 已提交
273

A
AUTOMATIC 已提交
274 275
    def run(self, p: StableDiffusionProcessing, *args):
        script_index = args[0]
A
AUTOMATIC 已提交
276

A
AUTOMATIC 已提交
277 278
        if script_index == 0:
            return None
A
AUTOMATIC 已提交
279

280
        script = self.selectable_scripts[script_index-1]
A
AUTOMATIC 已提交
281

A
AUTOMATIC 已提交
282 283
        if script is None:
            return None
A
AUTOMATIC 已提交
284

A
AUTOMATIC 已提交
285 286
        script_args = args[script.args_from:script.args_to]
        processed = script.run(p, *script_args)
A
AUTOMATIC 已提交
287

288 289
        shared.total_tqdm.clear()

A
AUTOMATIC 已提交
290
        return processed
A
AUTOMATIC 已提交
291

292 293 294 295 296 297 298 299 300
    def run_alwayson_scripts(self, p):
        for script in self.alwayson_scripts:
            try:
                script_args = p.script_args[script.args_from:script.args_to]
                script.process(p, *script_args)
            except Exception:
                print(f"Error running alwayson script: {script.filename}", file=sys.stderr)
                print(traceback.format_exc(), file=sys.stderr)

A
AUTOMATIC 已提交
301
    def reload_sources(self, cache):
D
DepFA 已提交
302 303 304 305 306 307
        for si, script in list(enumerate(self.scripts)):
            with open(script.filename, "r", encoding="utf8") as file:
                args_from = script.args_from
                args_to = script.args_to
                filename = script.filename
                text = file.read()
D
DepFA 已提交
308

D
DepFA 已提交
309
                from types import ModuleType
D
DepFA 已提交
310

A
AUTOMATIC 已提交
311 312 313 314 315 316
                module = cache.get(filename, None)
                if module is None:
                    compiled = compile(text, filename, 'exec')
                    module = ModuleType(script.filename)
                    exec(compiled, module.__dict__)
                    cache[filename] = module
D
DepFA 已提交
317 318 319 320 321 322 323

                for key, script_class in module.__dict__.items():
                    if type(script_class) == type and issubclass(script_class, Script):
                        self.scripts[si] = script_class()
                        self.scripts[si].filename = filename
                        self.scripts[si].args_from = args_from
                        self.scripts[si].args_to = args_to
A
AUTOMATIC 已提交
324

325

A
AUTOMATIC 已提交
326 327
scripts_txt2img = ScriptRunner()
scripts_img2img = ScriptRunner()
D
DepFA 已提交
328

329

D
DepFA 已提交
330
def reload_script_body_only():
A
AUTOMATIC 已提交
331 332 333
    cache = {}
    scripts_txt2img.reload_sources(cache)
    scripts_img2img.reload_sources(cache)
D
DepFA 已提交
334

D
DepFA 已提交
335

336
def reload_scripts():
D
DepFA 已提交
337
    global scripts_txt2img, scripts_img2img
D
DepFA 已提交
338

339
    load_scripts()
D
DepFA 已提交
340

D
DepFA 已提交
341 342
    scripts_txt2img = ScriptRunner()
    scripts_img2img = ScriptRunner()
343