ローカル PC でプロンプトから簡単に動画を生成します。
﻿import os, time
from const import Path
from config import Config
from l10n import L10n
from log import Log
from ui import Form
from model import Model
from controller import Controller
from task import Task
from prompt_travel import PromptTravel


class EasyPromptAnimeEditor:
    def __init__(self):
        Path.cwd = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
        os.chdir(Path.cwd)

        ffmpegBinPath = os.path.join(Path.cwd, "ffmpeg-master-latest-win64-gpl", "bin")
        os.environ["PATH"] = f'{ffmpegBinPath};{os.environ["PATH"]}'

        Form.loadConfig()
        self.form = Form()

        Model.loadConfig()
        self.model = Model()

        Task.initialize(self.form.win)

        self.controller = Controller(self.form, self.model)

        # TODO: Load file or default prompt
        self.model.prompt.loadDefaultPrompt()
        lastFrame = self.model.prompt.getLastPrompt()[0]
        if lastFrame > 0:
            self.model.generate.length = lastFrame + 10  # notifyAll
        self.model.notifyAll()

        self.form.win.protocol("WM_DELETE_WINDOW", self.onWinClose)
        self.form.win.after(50, self.saveConfig)
        PromptTravel.loadTemplate()

    def saveConfig(self):
        L10n.storeConfig()
        self.model.storeConfig()
        self.form.storeConfig()
        Config.save(Path.ini)

    def onWinClose(self):
        self.saveConfig()
        self.form.win.destroy()

    def run(self):
        self.form.run()


initStartTime = time.perf_counter()

Config.load(Path.ini, ["default", "ui", "ui_color", "ui_preview", "ui_size"])

L10n.load()

editor = EasyPromptAnimeEditor()

Log.user(L10n.format("log_initialization_end", time.perf_counter() - initStartTime))
Log.user(L10n.get("log_see_prompt_help"))
Log.user(L10n.get("news"))
editor.run()

editor/src/EasyPromptAnimeEditor.py
Zuntan03/EasyPromptAnime
﻿from configparser import ConfigParser


class Config:
    @classmethod
    def get(cls, cateroty, key, fallback):
        return cls._config.get(cateroty, key, fallback=str(fallback))

    @classmethod
    def getBool(cls, cateroty, key, fallback):
        return cls._config.getboolean(cateroty, key, fallback=fallback)

    @classmethod
    def getInt(cls, cateroty, key, fallback):
        return cls._config.getint(cateroty, key, fallback=fallback)

    @classmethod
    def getFloat(cls, cateroty, key, fallback):
        return cls._config.getfloat(cateroty, key, fallback=fallback)

    @classmethod
    def set(cls, cateroty, key, value):
        cls._config[cateroty][key] = str(value)

    @classmethod
    def load(cls, path, sections):
        cls._config = ConfigParser()
        [cls._config.add_section(section) for section in sections]
        cls._config.read(path, encoding="utf-8-sig")

    @classmethod
    def save(cls, path):
        with open(path, "w", encoding="utf-8-sig") as cfgfile:
            cls._config.write(cfgfile)

editor/src/config.py
Zuntan03/EasyPromptAnime
﻿import os, datetime


class Const:
    tile = "tile-upscale"
    refine = "refine"

    schedulerValues = [
        "k_dpmpp_2m_sde",
        "k_dpmpp_sde",
        "k_dpmpp_2m",
        "ddim",
        "euler",
        "euler_a",
    ]
    schedulerNames = [
        "DPM++ 2M SDE Karras",
        "DPM++ SDE Karras",
        "DPM++ 2M Karras",
        "DDIM",
        "Euler",
        "Euler a",
    ]

    @classmethod
    def getSchedulerValue(cls, name):
        return cls.schedulerValues[cls.schedulerNames.index(name)]

    @classmethod
    def getSchedulerName(cls, value):
        return cls.schedulerNames[cls.schedulerValues.index(value)]


class Path:
    cwd = os.getcwd()
    ini = "EasyPromptAnimeEditor.ini"
    output = "output"

    promptTravel = "animatediff-cli-prompt-travel"
    data = os.path.join(promptTravel, "data")
    model = os.path.join(data, "models", "sd")
    motionModule = os.path.join(data, "models", "motion-module")
    vae = os.path.join(data, "vae")
    lora = os.path.join(data, "lora")
    embeddings = os.path.join(data, "embeddings")
    wildcard = os.path.join(promptTravel, "wildcards")
    promptTravelOutput = os.path.join(promptTravel, "output")
    promptTravelUpscaled = os.path.join(promptTravel, "upscaled")
    promptTravelRefined = os.path.join(promptTravel, "refine")
    ipAdapter = os.path.join(data, "ip_adapter_image")

    editor = "editor"
    tmp = os.path.join(editor, "tmp")
    log = os.path.join(editor, "log")
    defaultPrompt = os.path.join(editor, "DefaultPrompt-0_1_0.txt")
    promptTravelTemplate = os.path.join(editor, "PromptTravelTemplate-0_2_0.txt")
    downloadMenu = os.path.join(editor, "DownloadMenu.json")

    @classmethod
    def getYYYYMMDDHHMMSS(cls):
        return datetime.datetime.now().strftime("%Y_%m%d_%H%M_%S")

    @classmethod
    def getYYYYMMDD(cls):
        return datetime.datetime.now().strftime("%Y_%m%d")

editor/src/const.py
Zuntan03/EasyPromptAnime
﻿import tkinter as tk

from ctr_menu import MenuController
from ctr_input import InputController
from ctr_preview import PreviewController
from ctr_basic import BasicController
from ctr_generate import GenerateController
from ctr_upscale import UpscaleController
from ctr_ip_adapter import IpAdapterController
from ctr_output import OutputController


class Controller:
    def __init__(self, form, model):
        self.form = form
        self.model = model

        self.menu = MenuController(form, model)
        self.input = InputController(form, model)
        self.preview = PreviewController(form, model)
        self.basic = BasicController(form, model)
        self.generate = GenerateController(form, model)
        self.upscale = UpscaleController(form, model)
        self.ipAdapter = IpAdapterController(form, model)
        self.output = OutputController(form, model)

        self.menu.initEvents()
        self.input.initEvents()
        self.preview.initEvents()

editor/src/controller.py
Zuntan03/EasyPromptAnime
﻿from const import Const
from log import Log
import tkinter as tk


class BasicController:
    def __init__(self, form, model):
        self.form = form
        self.model = model

        mGenerate = self.model.generate
        vBasic = self.form.basic
        mGenerate.bind("length", vBasic.varLength)
        mGenerate.bind("model", vBasic.varModel)
        mGenerate.bind("vae", vBasic.varVae)
        mGenerate.bind("seed", vBasic.varSeed)
        mGenerate.bind("width", vBasic.varWidth)
        mGenerate.bind("height", vBasic.varHeight)

        mGenerate.bind("upscale1Enabled", vBasic.varUpscale1Enabled)
        mGenerate.bind("upscale1Mode", vBasic.varUpscale1Mode)
        mGenerate.bind("upscale1Scale", vBasic.varUpscale1Scale)

        mGenerate.bind("upscale2Enabled", vBasic.varUpscale2Enabled)
        mGenerate.bind("upscale2Mode", vBasic.varUpscale2Mode)
        mGenerate.bind("upscale2Scale", vBasic.varUpscale2Scale)

editor/src/ctr_basic.py
Zuntan03/EasyPromptAnime
﻿from log import Log
import tkinter as tk


class GenerateController:
    def __init__(self, form, model):
        self.form = form
        self.model = model

        mGenerate = self.model.generate
        vGenerate = self.form.generate

        mGenerate.bind("motionModule", vGenerate.varMotionModule)
        mGenerate.bind("context", vGenerate.varContext)
        mGenerate.bind("scheduler", vGenerate.varScheduler)
        mGenerate.bind("steps", vGenerate.varSteps)
        mGenerate.bind("guidanceScale", vGenerate.varGuidanceScale)
        mGenerate.bind("clipSkip", vGenerate.varClipSkip)
        mGenerate.bind("promptFixedRatio", vGenerate.varPromptFixedRatio)
        mGenerate.bind("useHalfVae", vGenerate.varUseHalfVae)
        mGenerate.bind("useXFormers", vGenerate.varUseXFormers)

editor/src/ctr_generate.py
Zuntan03/EasyPromptAnime
﻿import tkinter as tk
from l10n import L10n
from task import Task
from tsk_generate import GenerateTask


class InputController:
    def __init__(self, form, model):
        self.form = form
        self.model = model

        vInput = self.form.input
        self.model.prompt.subsc("original", self.setTxtInput)
        vInput.txtInput.bind("<<Modified>>", self.modifiedTxtInput)
        self.bindKeyboardShortcuts()

        mEdit = self.model.editor
        mEdit.bind("previewUpscale", vInput.varUpscale)
        # mEdit.bind("previewInterpolation", vInput.varInterpolation)
        mEdit.bind("previewStart", vInput.varStart)
        mEdit.bind("previewLength", vInput.varLength)

        mEdit.bind("taskForever", vInput.varForever)
        mEdit.bind("taskPauseByError", vInput.varPauseByError)

        vInput.btnGenerate.configure(command=self.onGenerate)
        vInput.btnSeedGacha.configure(command=self.onSeedGacha)
        vInput.btnPreview.configure(command=self.onPreview)

    def setTxtInput(self, *args):
        vTxtInput = self.form.input.txtInput
        mOriginal = self.model.prompt.original
        if mOriginal == vTxtInput.get("1.0", tk.END):
            return
        vTxtInput.delete("1.0", tk.END)
        vTxtInput.insert(tk.END, mOriginal)

    def modifiedTxtInput(self, *args):
        vTxtInput = self.form.input.txtInput
        self.model.prompt.set("original", vTxtInput.get("1.0", tk.END))
        vTxtInput.edit_modified(False)

    def bindKeyboardShortcuts(self):
        pass  # TODO: txtInput shortcuts

    def onGenerate(self):
        GenerateTask.generate(self.model)

    def onSeedGacha(self):
        GenerateTask.seedGacha(self.model)

    def onPreview(self):
        GenerateTask.preview(self.model)

    def initEvents(self):
        self.model.editor.subsc("previewStart", self.onPreviewStartChanged)
        self.model.editor.subsc("previewLength", self.onPreviewLengthChanged)
        self.model.generate.subsc("length", self.onLengthChanged)

    def onPreviewStartChanged(self, *args):
        lengthTo = self.model.generate.length - self.model.editor.previewStart
        self.form.input.sliLength.configure(to=lengthTo)
        self.updatePreviewLengthLbl()

    def onPreviewLengthChanged(self, *args):
        startTo = self.model.generate.length - self.model.editor.previewLength
        self.form.input.sliStart.configure(to=startTo)
        self.updatePreviewLengthLbl()

    def onLengthChanged(self, *args):
        startTo = self.model.generate.length - self.model.editor.previewLength
        self.form.input.sliStart.configure(to=startTo)
        lengthTo = self.model.generate.length - self.model.editor.previewStart
        self.form.input.sliLength.configure(to=lengthTo)
        self.updatePreviewLengthLbl()

    def updatePreviewLengthLbl(self):
        start = int(self.model.editor.previewStart / 10)
        end = int(self.model.getPreviewEnd() / 10)
        lbl = f"{L10n.get('preview_length')}{start}-{end}{L10n.get('sec')}"
        self.form.input.lblLength.configure(text=lbl)

editor/src/ctr_input.py
Zuntan03/EasyPromptAnime
﻿from log import Log
import tkinter as tk


class IpAdapterController:
    def __init__(self, form, model):
        self.form = form
        self.model = model
        mGenerate = self.model.generate
        vIpAdapter = self.form.ipAdapter

        mGenerate.bind("useIpAdapter", vIpAdapter.varUse)
        mGenerate.bind("useIpAdapterPlus", vIpAdapter.varUsePlus)
        mGenerate.bind("useIpAdapterPlusFace", vIpAdapter.varUsePlusFace)
        mGenerate.bind("ipAdapterScale", vIpAdapter.varScale)
        mGenerate.bind("ipAdapterImageDir", vIpAdapter.varImageDir)

editor/src/ctr_ip_adapter.py
Zuntan03/EasyPromptAnime
﻿import os, subprocess
import tkinter as tk
from tkinter import filedialog
from const import Path
from l10n import L10n
from log import Log
from tsk_generate import GenerateTask
from tsk_upscale import UpscaleTask


class MenuController:
    def __init__(self, form, model):
        self.form = form
        self.model = model

    def initEvents(self):
        self.initFileEvents()
        self.initAnimeEvents()
        self.initFolderEvents()
        self.initToolEvents()
        self.initDownloadEvents()
        self.initHelpEvents()

    def initFileEvents(self):
        stf = self.form.menu.settingFileMenu
        self.openFile(stf, "m_open_ini_file", Path.ini)
        self.openFile(stf, "m_open_default_prompt_file", Path.defaultPrompt)
        self.openFile(
            stf, "m_open_prompt_travel_template_file", Path.promptTravelTemplate
        )

    def initAnimeEvents(self):
        anime = self.form.menu.animeMenu
        anime.entryconfig(
            L10n.get("m_anime_preview"),
            command=lambda: GenerateTask.preview(self.model),
        )
        anime.entryconfig(
            L10n.get("m_anime_seed_gacha"),
            command=lambda: GenerateTask.seedGacha(self.model),
        )
        anime.entryconfig(
            L10n.get("m_anime_generate"),
            command=lambda: GenerateTask.generate(self.model),
        )
        anime.entryconfig(
            L10n.get("m_anime_upscale"), command=lambda: self.upscale(False)
        )
        anime.entryconfig(
            L10n.get("m_anime_upscale_config"), command=lambda: self.upscale(True)
        )

    def upscale(self, configOverride):
        Log.user(
            L10n.get("log_upscale_config_override" if configOverride else "log_upscale")
        )
        upscaleDir = filedialog.askdirectory(
            title=L10n.get("dlg_upscale_title"), initialdir=Path.promptTravelOutput
        )

        if upscaleDir == "":
            return
        if configOverride:
            UpscaleTask.upscaleWithConfigOverride(self.model, upscaleDir)
        else:
            UpscaleTask.upscale(self.model, upscaleDir)

    def initFolderEvents(self):
        fld = self.form.menu.folderMenu
        self.openFolder(fld, "m_output_folder", Path.output)

        ptf = self.form.menu.promptTravelFolderMenu
        self.openFolder(ptf, "m_sd_model_folder", Path.model)
        self.openFolder(ptf, "m_motion_module_folder", Path.motionModule)
        self.openFolder(ptf, "m_lora_folder", Path.lora)
        self.openFolder(ptf, "m_embeddings_folder", Path.embeddings)
        self.openFolder(ptf, "m_wildcard_folder", Path.wildcard)

        ptof = self.form.menu.promptTravelOutputFolderMenu
        self.openFolder(
            ptof, "m_prompt_travel_generate_folder", Path.promptTravelOutput
        )
        self.openFolder(ptof, "m_prompt_travel_tile_folder", Path.promptTravelUpscaled)
        self.openFolder(ptof, "m_prompt_travel_refine_folder", Path.promptTravelRefined)

        self.openFolder(fld, "m_temp_folder", Path.tmp)
        self.openFolder(fld, "m_log_folder", Path.log)

    def initToolEvents(self):
        tool = self.form.menu.toolMenu
        tool.entryconfig(
            L10n.get("m_tool_convert_lora"), command=lambda: self.convertLora()
        )

        self.openUrl(tool, "m_tool_easy_leco", L10n.get("m_tool_easy_leco_url"))

    def convertLora(self):
        srcPath = filedialog.askopenfilename(
            title=L10n.get("dlg_convert_lora_title"),
            filetypes=[("C3Lier LoRA or LoCon LoRA", "*.safetensors")],
            initialdir=Path.lora,
        )
        if srcPath is None or srcPath == "":
            return
        srcPath = os.path.abspath(srcPath)
        modelPath = os.path.abspath(os.path.join(Path.model, self.model.generate.model))
        dim = 32
        dstPath = os.path.abspath(os.path.join(Path.lora, os.path.basename(srcPath)))
        Log.user(L10n.format("log_convert_lora", dim, srcPath, modelPath, dstPath))
        subprocess.run(
            ["start", "cmd", "/c", "ConvertLora.bat", srcPath, f"{dim}", modelPath],
            shell=True,
        )

    def initDownloadEvents(self):
        self.categoryDirs = {
            "model": Path.model,
            "motion_module": Path.motionModule,
            "vae": Path.vae,
            "embedding": Path.embeddings,
        }
        dlMenu = self.form.menu.downloadMenu
        dlMenu.configure(postcommand=self.updateDownloadMenu)

        for category, categoryData in self.form.menu.downloadMenuData.items():
            for name, data in categoryData.items():
                label = f'({data["tag"]}) {name}' if "tag" in data else name
                data["menu"].entryconfig(
                    label,
                    command=lambda c=category, n=name, d=data: self.download(c, n, d),
                )

    def updateDownloadMenu(self):
        for category, categoryData in self.form.menu.downloadMenuData.items():
            for name, data in categoryData.items():
                dstPath = os.path.join(self.categoryDirs[category], name)
                data["checked"].set(os.path.exists(dstPath))

    def download(self, category, name, data):
        dstPath = os.path.join(self.categoryDirs[category], name)
        unzipDir = ""
        if "isZip" in data and data["isZip"]:
            unzipDir = os.path.dirname(dstPath)
            dstPath = os.path.join(os.path.dirname(dstPath), "temp.zip")

        Log.user(L10n.format("log_start_download", name, data["info"]))
        cmd = f'echo {data["info"]} & curl -Lo {dstPath} {data["url"]} || pause'
        if unzipDir != "":
            cmd += f" & PowerShell -Version 5.1 -ExecutionPolicy Bypass Expand-Archive -Path {dstPath} -DestinationPath {unzipDir} -Force || pause & del {dstPath}"
        subprocess.run(["start", "cmd", "/c", cmd], shell=True)

    def initHelpEvents(self):
        hlp = self.form.menu.helpMenu
        hlp.entryconfig(
            L10n.get("m_prompt_help"),
            command=lambda: self.form.input.txtInput.insert(
                tk.END, L10n.get("hlp_prompt")
            ),
        )

        self.openUrl(hlp, "m_github", "https://github.com/Zuntan03/EasyPromptAnime")

        ref = self.form.menu.referenceMenu
        self.openUrlNoL10n(
            ref,
            "AnimateDiff prompt travel",
            "https://github.com/s9roll7/animatediff-cli-prompt-travel",
        )
        self.openUrlNoL10n(ref, "sd-scripts", "https://github.com/kohya-ss/sd-scripts")
        self.openUrlNoL10n(ref, "Codex FFmpeg", "https://github.com/GyanD/codexFFmpeg")
        self.openUrlNoL10n(
            ref, "ECCV2022-RIFE", "https://github.com/megvii-research/ECCV2022-RIFE"
        )

    def openFile(self, menu, name, path):
        menu.entryconfig(
            L10n.get(name),
            command=lambda: subprocess.run(["start", path], shell=True),
        )

    def openFolder(self, menu, name, path):
        menu.entryconfig(
            L10n.get(name),
            command=lambda: subprocess.run(["explorer", path]),
        )

    def openUrl(self, menu, name, url):
        menu.entryconfig(
            L10n.get(name), command=lambda: subprocess.run(["start", url], shell=True)
        )

    def openUrlNoL10n(self, menu, name, url):
        menu.entryconfig(
            name, command=lambda: subprocess.run(["start", url], shell=True)
        )

editor/src/ctr_menu.py
Zuntan03/EasyPromptAnime
﻿from log import Log
import tkinter as tk


class OutputController:
    def __init__(self, form, model):
        self.form = form
        self.model = model
        Log.systemLogEvent.append(self.onSystemLog)
        Log.userLogEvent.append(self.onUserLog)

    def onSystemLog(self, msg):
        op = self.form.output
        op.txtSys.configure(state=tk.NORMAL)
        op.txtSys.insert(tk.END, msg)
        op.txtSys.see(tk.END)
        op.txtSys.insert(tk.END, "\n")
        op.txtSys.configure(state=tk.DISABLED)

    def onUserLog(self, msg):
        op = self.form.output
        op.txtUser.configure(state=tk.NORMAL)
        op.txtUser.insert(tk.END, msg)
        op.txtUser.see(tk.END)
        op.txtUser.insert(tk.END, "\n")
        op.txtUser.configure(state=tk.DISABLED)

editor/src/ctr_output.py
Zuntan03/EasyPromptAnime
﻿import tkinter as tk


class PreviewController:
    def __init__(self, form, model):
        self.form = form
        self.model = model

        mEdit = self.model.editor
        vPrev = self.form.preview
        mEdit.bind("previewShowKeyframe", vPrev.varShowKeyframe)
        mEdit.bind("previewShowHeaderFooter", vPrev.varShowHeaderFooter)
        mEdit.bind("previewShowAnime", vPrev.varShowAnime)

    def initEvents(self):
        self.initPreview()

    def initPreview(self):
        self.model.prompt.subsc("original", self.updatePerview)
        self.model.generate.subsc("length", self.updatePerview)

        mEdit = self.model.editor
        mEdit.subsc("previewShowKeyframe", self.updatePerview)
        mEdit.subsc("previewShowHeaderFooter", self.updatePerview)
        mEdit.subsc("previewShowAnime", self.updatePerview)
        mEdit.subsc("previewStart", self.updatePerview)
        mEdit.subsc("previewLength", self.updatePerview)

    def updatePerview(self, *args):
        mEdit = self.model.editor
        prevTxt = self.model.prompt.getPreview(
            mEdit.previewStart,
            self.model.getPreviewLength(),
            mEdit.previewShowKeyframe,
            mEdit.previewShowHeaderFooter,
            mEdit.previewShowAnime,
        )

        vTxtPrev = self.form.preview.txtPrev
        vTxtPrev.configure(state=tk.NORMAL)
        vTxtPrev.delete("1.0", tk.END)
        vTxtPrev.insert(tk.END, prevTxt)
        vTxtPrev.configure(state=tk.DISABLED)

editor/src/ctr_preview.py
Zuntan03/EasyPromptAnime
﻿from log import Log
import tkinter as tk


class UpscaleController:
    def __init__(self, form, model):
        self.form = form
        self.model = model
        mGenerate = self.model.generate
        vUpscale = self.form.upscale

        mGenerate.bind("upscaleScheduler", vUpscale.varScheduler)
        mGenerate.bind("upscaleSteps", vUpscale.varSteps)
        mGenerate.bind("upscaleGuidanceScale", vUpscale.varGuidanceScale)
        mGenerate.bind("upscaleStrength", vUpscale.varDenoise)
        mGenerate.bind("upscaleTileScale", vUpscale.varTileScale)
        mGenerate.bind("upscaleTileStart", vUpscale.varTileStart)
        mGenerate.bind("upscaleTileEnd", vUpscale.varTileEnd)
        mGenerate.bind("upscaleUseHalfVae", vUpscale.varUseHalfVae)
        mGenerate.bind("upscaleUseXFormers", vUpscale.varUseXFormers)

editor/src/ctr_upscale.py
Zuntan03/EasyPromptAnime
﻿from config import Config
from l10n_en import l10nEn
from l10n_ja import l10nJa
import locale


class L10n:
    @classmethod
    def get(cls, key):
        return cls._l10n[key]

    @classmethod
    def format(cls, key, *args):
        return cls._l10n[key].format(*args)

    @classmethod
    def load(cls):
        lang = Config.get("ui", "lang", fallback=locale.getdefaultlocale()[0][:2])
        cls._l10n = l10nEn
        if lang == "ja":
            cls._l10n = l10nJa

    @classmethod
    def storeConfig(cls):
        Config.set("ui", "lang", cls.get("lang"))

editor/src/l10n.py
Zuntan03/EasyPromptAnime
﻿l10nEn = {
    "lang": "en",
    "news": 'Added upscaling function from sequential png to "Anime" menu. You can fix the black screen when upscaling, or upscale the model etc. with different settings.',
    "error": "Error",
    "sec": "s",
    "title": "EasyPromptAnime",
    "clear": "Clear",
    "negative_prompt": "Negative prompt",
    "swap": "Swap",
    "generate_anime": "Generate anime",
    "upscale": "Upscale",
    "seed_gacha": "Seed Gacha",
    "preview": "Preview",
    "preview_upscale": "Upscale",
    # "preview_interpolation": "Interpolation",
    "preview_start_frame": "Preview\nStart",
    "preview_length": "Length\n",
    "task_forever": "Forever",
    "task_pause_by_error": "Pause by error",
    "preview_show_keyframe": "Show keyframe",
    "preview_show_header_footer": "Show header & footer",
    "preview_show_anime": "Show anime",
    "generate_length": "Length\n{0:>7}s",
    "generate_seed": "Seed",
    "generate_model": "Model",
    "generate_vae": "VAE",
    "generate_width": "Width",
    "generate_height": "Height",
    "generate_motion_module": "Motion\nmodule",
    "generate_context": "Context",
    "generate_scheduler": "Scheduler",
    "generate_steps": "Steps",
    "generate_guidance_scale": "Guidance scale\n(CFG scale)",
    "generate_clip_skip": "Clip\nskip",
    "generate_prompt_fixed_ratio": "Prompt\nfixed ratio",
    "generate_use_half_vae": "Use Half VAE",
    "generate_use_x_formers": "Use xFormers",
    "upscale1": "1st upscale",
    "upscale2": "2nd upscale",
    "upscale_scale": "Scale",
    "upscale_scheduler": "Scheduler",
    "upscale_steps": "Steps",
    "upscale_guidance_scale": "Guidance scale\n(CFG scale)",
    "upscale_tile_scale": "Tile\nscale",
    "upscale_tile_start": "Tile\nstart",
    "upscale_tile_end": "Tile\nend",
    "upscale_use_half_vae": "Use Half VAE",
    "upscale_use_x_formers": "Use xFormers",
    "upscale_strength": "Denoise strength",
    "ip_adapter_use": "Use ImagePrompt-Adapter",
    "ip_adapter_use_plus": "Use IP-Adapter Plus",
    "ip_adapter_use_plus_face": "Use IP-Adapter Plus Face",
    "ip_adapter_scale": "Image prompt ratio\n(prompt required even for 1.0)",
    "ip_adapter_image_dir": "Image folder for IP",
    "ip_adapter_image_dir_explorer": "Open in Explorer",
    "ip_adapter_info": "Create a folder in ip_adapter_image/ in [Open in Explorer] and place the png image for the IP adapter.\nThe file name of a png image specifies the number of frames to use as an image prompt,\nsuch as 000.png (0 frame) or 010.png (10frame 1sec).",
    "preview_tab": "Preview",
    "basic_tab": "Basic",
    "generate_tab": "Generate anime",
    "upscale_tab": "Upscale",
    "ip_adapter_tab": "IP-Adapter",
    "user_log_tab": "User Log",
    "system_log_tab": "System Log",
    "m_file": "File",
    "m_setting_file": "Setting files",
    "m_open_ini_file": "Open EasyPromptAnimeEditor.ini",
    "m_open_default_prompt_file": "Open default prompt file",
    "m_open_prompt_travel_template_file": "Open prompt travel template file",
    "m_anime": "Anime",
    "m_anime_preview": "Preview",
    "m_anime_seed_gacha": "Seed Gacha",
    "m_anime_generate": "Generate anime",
    "m_anime_upscale": "1st upscale with settings at generation",
    "m_anime_upscale_config": "1st upscale with current settings",
    "m_folder": "Folder",
    "m_output_folder": "Output",
    "m_prompt_travel_folder": "Prompt travel",
    "m_sd_model_folder": "Model",
    "m_motion_module_folder": "Motion module",
    "m_lora_folder": "LoRA",
    "m_embeddings_folder": "TI (Embedding)",
    "m_wildcard_folder": "Wildcard",
    "m_prompt_travel_output_folder": "Prompt travel output",
    "m_prompt_travel_generate_folder": "Generate anime",
    "m_prompt_travel_tile_folder": "Tile upscale",
    "m_prompt_travel_refine_folder": "Refine upscale",
    "m_temp_folder": "Temp",
    "m_log_folder": "Log",
    "m_tool": "Tools",
    "m_tool_convert_lora": "Convert unusable LoRA to usable",
    "m_tool_easy_leco": "Create LoRA from prompts with EasyLECO",
    "m_tool_easy_leco_url": "https://colab.research.google.com/drive/1cEoOeLbGmzCK5dP-Xa6Yx0B53uS461J8",
    "m_download": "Download",
    "m_dl_model": "Model",
    "m_dl_motion_module": "Motion module",
    "m_dl_vae": "VAE",
    "m_dl_embedding": "TI (Embedding)",
    "m_help": "Help",
    "m_prompt_help": "How to write prompts",
    "m_github": "EasyPromptAnime GitHub",
    "m_reference": "Reference",
    "dlg_convert_lora_title": "Select LoRA to convert.",
    "dlg_upscale_title": "Select the folder (00-seed~) containing the sequentially numbered png to be upscaled.",
    "log_initialization_end": "Initialized: {0:.2f}s.",
    "log_see_prompt_help": "See [Help] menu [How to write prompts] for how to write prompts.",
    "log_task_start": "Started {0}.",
    "log_task_success": "Completed {0}. {1:.2f}s.",
    "log_task_failed": "Failed {0}. {1:.2f}s.\nEnable [Pause by error] and check the error.",
    "log_ffplay": "Preview seed {0}. You can change ffplay command in EasyPromptAnimeEditor.ini.",
    "log_upscale": "Upscale sequentially numbered png with 1st upscale method and height.",
    "log_upscale_config_override": "Upscale sequentially numbered png using all settings for the 1st upscale.",
    "log_convert_lora": "Convert LoRA with Dim {0}.\nOriginal LoRA: {1}\nMerge model for extraction: {2}\nConverted LoRA: {3}",
    "log_start_download": "Start download: {0} {1}",
    "err_inv_line": "Invalid line. {0}",
    "err_inv_frame_num": "Invalid frame number. {0}: {1}",
    "err_inv_lora_def": 'Invalid LoRA definition. "{0}" {1}',
    "hlp_prompt": """
# The number of frames + colon (:) + prompt specifies the prompt for the animation keyframe.
# Instead of frame number + colon, "H:", "F:", "N:", and "L:" will set special settings for the entire animation.
# Sharp (#) and after are comments.

H: crowds, akihabara
# Lines starting with "H:" or "h:" specify header prompts, which are added at the beginning of the keyframe prompts in the animation.

 0: standing, upset
10: waving at viewer, surprised
20: waving at viewer, smile
# Use the prompts "standing, upset" at frame 0 of the animation, "having at viewer, surprised" at frame 10, and "having at viewer, smile" at frame 20.
# 10 images are displayed per second (10 frames per second), with interpolation between key frames.

F: 1girl, maid outfit
# Lines that start with "F:" or "f:" specify footer prompts, which are added to the end of the keyframe prompts in the animation.

# When generated for 3 seconds (30 frames) with the above specifications, the keyframe prompt will be as follows.
#  0 frame "crowds, akihabara, standing, upset, 1girl, maid outfit"
# 10 frame "crowds, akihabara, waving at viewer, surprised, 1girl, maid outfit"
# 20 frame "crowds, akihabara, waving at viewer, smile, 1girl, maid outfit"
# 30 frame "crowds, akihabara, standing, upset, 1girl, maid outfit"

# You do not need to set keyframes in detail, just put a word that expresses movement, such as "dancing," in frame 0, and the AI will move the image on its own.

N:(worst quality, low quality:1.4)
# Lines beginning with "N:" or "n:" specify negative prompts to be used for all frames of the animation.

# L:explosion:-0.2 # The # at the beginning of the line is a comment. If you use LoRA, please remove the # at the beginning.
# Lines beginning with "L:" or "l:" may contain multiple LoRA file names + colon (:) + intensity, separated by commas (,).
# If you want to use LoRA, please put LoRA in *.safetensors format in the menu [Folder - Prompt travel - LoRA].
# The available LoRA is the normal LierLa format. C3Lier(Locon) format can be converted to LierLa format using the menu [Tools - Convert LoRA].
""",
}

editor/src/l10n_en.py
Zuntan03/EasyPromptAnime
﻿l10nJa = {
    "lang": "ja",
    "news": "[アニメ] メニューに連番pngからアップスケール機能を追加しました。アップスケールでの黒画面を修正したり、モデルなどを異なる設定にしてアップスケールできます。",
    "error": "エラー",
    "sec": "秒",
    "title": "簡単プロンプトアニメ",
    "clear": "クリア",
    "negative_prompt": "ネガティブプロンプト",
    "swap": "入替",
    "generate_anime": "アニメ生成",
    "upscale": "アップスケール",
    "seed_gacha": "シードガチャ",
    "preview": "プレビュー",
    "preview_upscale": "アップスケール",
    # "preview_interpolation": "補間",
    "preview_start_frame": "プレビュー\n開始",
    "preview_length": "長さ 　\n",
    "task_forever": "連続実行",
    "task_pause_by_error": "エラーで一時停止",
    "preview_show_keyframe": "キーフレームを表示",
    "preview_show_header_footer": "ヘッダとフッタを表示",
    "preview_show_anime": "アニメを表示",
    "generate_length": "フレーム数\n{0:>8}秒",
    "generate_seed": "シード",
    "generate_model": "モデル",
    "generate_vae": "VAE",
    "generate_width": "幅",
    "generate_height": "高さ",
    "generate_motion_module": "モーション\nモジュール",
    "generate_context": "コンテキスト",
    "generate_scheduler": "スケジューラ",
    "generate_steps": "ステップ数",
    "generate_guidance_scale": "ガイダンススケール\n(CFG スケール)",
    "generate_clip_skip": "クリップ\nスキップ",
    "generate_prompt_fixed_ratio": "プロンプト\n固定割合",
    "generate_use_half_vae": "Half VAE を使用",
    "generate_use_x_formers": "xFormers を使用",
    "upscale1": "初回アップスケール",
    "upscale2": "二重アップスケール",
    "upscale_scale": "倍率",
    "upscale_scheduler": "スケジューラ",
    "upscale_steps": "ステップ数",
    "upscale_strength": "ノイズ除去強度",
    "upscale_guidance_scale": "ガイダンススケール\n(CFG スケール)",
    "upscale_tile_scale": "タイル\n強度",
    "upscale_tile_start": "タイル\n開始",
    "upscale_tile_end": "タイル\n終了",
    "upscale_use_half_vae": "Half VAE を使用（黒画面出力の抑制事例あり）",
    "upscale_use_x_formers": "xFormers を使用",
    "ip_adapter_use": "イメージプロンプト(IP)アダプタを使用",
    "ip_adapter_use_plus": "IPアダプタプラスを使用",
    "ip_adapter_use_plus_face": "IPアダプタプラスフェイスを使用",
    "ip_adapter_scale": "イメージプロンプト比率\n（1.0でも要プロンプト指定）",
    "ip_adapter_image_dir": "IP用の画像フォルダ",
    "ip_adapter_image_dir_explorer": "エクスプローラで開く",
    "ip_adapter_info": "[エクスプローラで開く] の ip_adapter_image/ にフォルダを作成して、IPアダプタ用の png 画像を置きます。\npng 画像のファイル名は 000.png (0フレーム) や 010.png (10フレーム、1秒) のように、\nイメージプロンプトとして使用するフレーム数を指定します。",
    "preview_tab": "プレビュー",
    "basic_tab": "基本",
    "generate_tab": "アニメ生成",
    "upscale_tab": "アップスケール",
    "ip_adapter_tab": "IPアダプタ",
    "user_log_tab": "ユーザーログ",
    "system_log_tab": "システムログ",
    "m_file": "ファイル",
    "m_setting_file": "設定ファイル",
    "m_open_ini_file": "簡単プロンプトアニメの設定 ini ファイルを開く",
    "m_open_default_prompt_file": "デフォルトのプロンプトファイルを開く",
    "m_open_prompt_travel_template_file": "プロンプトトラベルのテンプレートファイルを開く",
    "m_anime": "アニメ",
    "m_anime_preview": "プレビュー",
    "m_anime_seed_gacha": "シードガチャ",
    "m_anime_generate": "アニメ生成",
    "m_anime_upscale": "生成時の設定で初回アップスケール",
    "m_anime_upscale_config": "現在の設定で初回アップスケール",
    "m_folder": "フォルダ",
    "m_output_folder": "アニメ出力先",
    "m_prompt_travel_folder": "プロンプトトラベル",
    "m_sd_model_folder": "モデル",
    "m_motion_module_folder": "モーションモジュール",
    "m_lora_folder": "LoRA",
    "m_embeddings_folder": "TI (Embedding)",
    "m_wildcard_folder": "ワイルドカード",
    "m_prompt_travel_output_folder": "プロンプトトラベル出力",
    "m_prompt_travel_generate_folder": "アニメ生成",
    "m_prompt_travel_tile_folder": "タイルアップスケール",
    "m_prompt_travel_refine_folder": "リファインアップスケール",
    "m_temp_folder": "テンポラリ",
    "m_log_folder": "ログ",
    "m_tool": "ツール",
    "m_tool_convert_lora": "使えない LoRA を使えるように変換",
    "m_tool_easy_leco": "簡単LECO でプロンプトから LoRA を作成",
    "m_tool_easy_leco_url": "https://colab.research.google.com/drive/1LYi2kvZV1XN05MATqhqWs79cNdTkb33P",
    "m_download": "ダウンロード",
    "m_dl_model": "モデル",
    "m_dl_motion_module": "モーションモジュール",
    "m_dl_vae": "VAE",
    "m_dl_embedding": "TI (Embedding)",
    "m_help": "ヘルプ",
    "m_prompt_help": "プロンプトの書き方",
    "m_github": "簡単プロンプトアニメ GitHub",
    "m_reference": "参照",
    "dlg_convert_lora_title": "変換する LoRA を選択してください。",
    "dlg_upscale_title": "アップスケールする連番 png の入ったフォルダ（00-シード値～）を選択してください。",
    "log_initialization_end": "起動に掛かった時間: {0:.2f}秒",
    "log_see_prompt_help": "プロンプトの書き方は [ヘルプ] メニューの [プロンプトの書き方] を参照してください。",
    "log_task_start": "{0} を開始しました。",
    "log_task_success": "{0} に成功しました。 {1:.2f}秒",
    "log_task_failed": "{0} に失敗しました。 {1:.2f}秒\n[エラーで一時停止] を有効にして、エラーの内容を確認してください。",
    "log_ffplay": "シード値 {0} を再生します。\n再生設定は EasyPromptAnimeEditor.ini の ffplay_cmd で変更できます。",
    "log_upscale": "現在の初回アップスケールの方式と高さの設定で、連番pngをアップスケールします。",
    "log_upscale_config_override": "現在の初回アップスケールの全設定（モデルなども含む）で、連番pngをアップスケールします。",
    "log_convert_lora": "LoRA を dim:{0} で変換します。\n変換元 LoRA: {1}\n抽出用マージモデル: {2}\n出力先 LoRA: {3}",
    "log_start_download": "{0} のダウンロードを開始します。\n{1}",
    "err_inv_line": "行の表記が正しくありません。 {0}",
    "err_inv_frame_num": "フレームの値が正しくありません。{0}: {1}",
    "err_inv_lora_def": 'LoRA の定義が正しくありません。"{0}" {1}',
    "hlp_prompt": """
# フレーム数＋コロン(:)＋プロンプトで、アニメのキーフレームにプロンプトを指定します。
# フレーム数＋コロンではなく、「H:」「F:」「N:」「L:」の場合はアニメ全体に特別な設定をします。
# シャープ(#)以降はコメントです。

H: crowds, akihabara
# 「H:」か「h:」で始まる行はヘッダプロンプトの指定で、アニメのキーフレームのプロンプト先頭にヘッダプロンプトを足します。

 0: standing, upset
10: waving at viewer, surprised
20: waving at viewer, smile
# アニメの 0フレームに「standing, upset」、10フレームに「waving at viewer, surprised」、20フレームに「waving at viewer, smile」のプロンプトを使用します。
# 1秒間に 10枚の画像が表示され(10 Frame per second)で、キーフレームの間は補間されます。

F: 1girl, maid outfit
# 「F:」か「f:」で始まる行はフッタプロンプトの指定で、アニメのキーフレームのプロンプト末尾にフッタプロンプトを足します。

# 上記の指定で3秒間（30フレーム）アニメ生成した場合の、キーフレームのプロンプトは以下になります。
#  0フレーム「crowds, akihabara, standing, upset, 1girl, maid outfit」
# 10フレーム「crowds, akihabara, waving at viewer, surprised, 1girl, maid outfit」
# 20フレーム「crowds, akihabara, waving at viewer, smile, 1girl, maid outfit」
# 30フレーム「crowds, akihabara, standing, upset, 1girl, maid outfit」

# 細々とキーフレームを設定しなくとも、0フレームに「dancing」のような動きを表す言葉を入れておけば、AI が勝手に動かしてくれます。

N:(worst quality, low quality:1.4)
#「N:」か「n:」で始まる行は、アニメの全フレームで使用するネガティブプロンプトを指定します。

# L:explosion:-0.2 # 行頭の # でコメントになっています。LoRA を使う場合は先頭の # を消してください。
#「L:」か「l:」で始まる行は、LoRA のファイル名＋コロン(:)＋強度を、カンマ(,)区切りで複数指定できます。
# LoRAを使いたい場合は、メニューの [フォルダ - プロンプトトラベル - LoRA] に *.safetensors 形式の LoRA を置いてください。
# 使える LoRA は通常の LierLa 形式です。C3Lier(Locon) 形式はメニューの [ツール - LoRA 変換] で LierLa 形式に変換して利用できます。
""",
}

editor/src/l10n_ja.py
Zuntan03/EasyPromptAnime
﻿class Log:
    userLogEvent = []
    systemLogEvent = []

    @classmethod
    def user(cls, msg):
        # msgNl = msg if msg.endswith("\n") else msg + "\n"
        # msgNoNewLine = (msg[:-1] if msg.endswith("\n") else msg)

        [hadler(msg) for hadler in cls.userLogEvent]
        cls.system(msg)

    @classmethod
    def system(cls, msg):
        # msgNl = msg if msg.endswith("\n") else msg + "\n"
        # msgNoNewLine = (msg[:-1] if msg.endswith("\n") else msg)
        [hadler(msg) for hadler in cls.systemLogEvent]
        print(msg)

    # @classmethod
    # def _system_impl(cls, msg):
    #     [hadler(msg) for hadler in cls.systemLogEvent]
    #     print(msg)

editor/src/log.py
Zuntan03/EasyPromptAnime
﻿from config import Config
from mdl_notifier import Notifier


class Editor(Notifier):
    defPreviewUpscale = True
    # defPreviewInterpolation = True
    defPreviewStart = 0
    defPreviewLength = 0

    defPreviewShowKeyframe = True
    defPreviewShowHeaderFooter = True
    defPreviewShowAnime = True

    defTaskForever = False
    defTaskPauseByError = True

    defFfplayCmd = "ffplay.exe -left 160 -top 80 -loop 5 -autoexit -loglevel error"

    def __init__(self):
        super().__init__()
        self.previewUpscale = Editor.defPreviewUpscale
        # self.previewInterpolation = Editor.defPreviewInterpolation
        self.previewStart = Editor.defPreviewStart
        self.previewLength = Editor.defPreviewLength

        self.previewShowKeyframe = Editor.defPreviewShowKeyframe
        self.previewShowHeaderFooter = Editor.defPreviewShowHeaderFooter
        self.previewShowAnime = Editor.defPreviewShowAnime

        self.taskForever = Editor.defTaskForever
        self.taskPauseByError = Editor.defTaskPauseByError

        self.ffplayCmd = Editor.defFfplayCmd

    @classmethod
    def loadConfig(cls):
        Editor.defPreviewUpscale = Config.getBool(
            "ui_preview", "upscale", Editor.defPreviewUpscale
        )
        # Editor.defPreviewInterpolation = Config.getBool(
        #     "ui_preview", "interpolation", Editor.defPreviewInterpolation
        # )
        Editor.defPreviewStart = Config.getInt(
            "ui_preview", "start", Editor.defPreviewStart
        )
        Editor.defPreviewLength = Config.getInt(
            "ui_preview", "length", Editor.defPreviewLength
        )

        Editor.defPreviewShowKeyframe = Config.getBool(
            "ui_preview", "show_keyframe", Editor.defPreviewShowKeyframe
        )
        Editor.defPreviewShowHeaderFooter = Config.getBool(
            "ui_preview", "show_header_footer", Editor.defPreviewShowHeaderFooter
        )
        Editor.defPreviewShowAnime = Config.getBool(
            "ui_preview", "show_anime", Editor.defPreviewShowAnime
        )

        Editor.defTaskForever = Config.getBool("ui", "forever", Editor.defTaskForever)
        Editor.defTaskPauseByError = Config.getBool(
            "ui", "pause_by_error", Editor.defTaskPauseByError
        )

        Editor.defFfplayCmd = Config.get("ui", "ffplay_cmd", Editor.defFfplayCmd)

    def storeConfig(self):
        Config.set("ui_preview", "upscale", Editor.defPreviewUpscale)
        # Config.set("ui_preview", "interpolation", Editor.defPreviewInterpolation)
        Config.set("ui_preview", "start", Editor.defPreviewStart)
        Config.set("ui_preview", "length", Editor.defPreviewLength)

        Config.set("ui_preview", "show_keyframe", Editor.defPreviewShowKeyframe)
        Config.set(
            "ui_preview", "show_header_footer", Editor.defPreviewShowHeaderFooter
        )
        Config.set("ui_preview", "show_anime", Editor.defPreviewShowAnime)

        Config.set("ui", "forever", Editor.defTaskForever)
        Config.set("ui", "pause_by_error", Editor.defTaskPauseByError)

        Config.set("ui", "ffplay_cmd", Editor.defFfplayCmd)

editor/src/mdl_editor.py
Zuntan03/EasyPromptAnime
﻿from const import Const
from config import Config
from mdl_notifier import Notifier


class Generate(Notifier):
    defLength = 30
    defModel = "real_model_N.safetensors"
    defVae = "kl-f8-anime2.ckpt"
    defMotionModule = "mm_sd_v15_v2.ckpt"
    defContext = 16
    defScheduler = "DPM++ SDE Karras"
    defSteps = 20
    defGuidanceScale = 8.0
    defClipSkip = 2
    defPromptFixedRatio = 0.5
    defUseHalfVae = False
    defUseXFormers = False
    defWidth = 384
    defHeight = 512
    defSeed = -1

    defUpscale1Enabled = True
    defUpscale1Mode = Const.tile
    defUpscale1Scale = 2.0

    defUpscale2Enabled = False
    defUpscale2Mode = Const.tile
    defUpscale2Scale = 1.5

    defUpscaleScheduler = "DPM++ SDE Karras"
    defUpscaleSteps = 15
    defUpscaleGuidanceScale = 6.0
    defUpscaleStrength = 0.6
    defUpscaleTileScale = 1.0
    defUpscaleTileStart = 0.0
    defUpscaleTileEnd = 1.0
    defUpscaleUseHalfVae = False
    defUpscaleUseXFormers = False
    defUseIpAdapter = False
    defUseIpAdapterPlus = True
    defUseIpAdapterPlusFace = True
    defIpAdapterScale = 0.5
    defIpAdapterImageDir = "test"

    def __init__(self):
        super().__init__()
        self.length = Generate.defLength
        self.model = Generate.defModel
        self.vae = Generate.defVae
        self.motionModule = Generate.defMotionModule
        self.context = Generate.defContext
        self.scheduler = Generate.defScheduler
        self.steps = Generate.defSteps
        self.guidanceScale = Generate.defGuidanceScale
        self.clipSkip = Generate.defClipSkip
        self.promptFixedRatio = Generate.defPromptFixedRatio
        self.useHalfVae = Generate.defUseHalfVae
        self.useXFormers = Generate.defUseXFormers
        self.width = Generate.defWidth
        self.height = Generate.defHeight
        self.seed = Generate.defSeed

        self.upscale1Enabled = Generate.defUpscale1Enabled
        self.upscale1Mode = Generate.defUpscale1Mode
        self.upscale1Scale = Generate.defUpscale1Scale

        self.upscale2Enabled = Generate.defUpscale2Enabled
        self.upscale2Mode = Generate.defUpscale2Mode
        self.upscale2Scale = Generate.defUpscale2Scale

        self.upscaleScheduler = Generate.defUpscaleScheduler
        self.upscaleSteps = Generate.defUpscaleSteps
        self.upscaleGuidanceScale = Generate.defUpscaleGuidanceScale
        self.upscaleStrength = Generate.defUpscaleStrength
        self.upscaleTileScale = Generate.defUpscaleTileScale
        self.upscaleTileStart = Generate.defUpscaleTileStart
        self.upscaleTileEnd = Generate.defUpscaleTileEnd
        self.upscaleUseHalfVae = Generate.defUpscaleUseHalfVae
        self.upscaleUseXFormers = Generate.defUpscaleUseXFormers

        self.useIpAdapter = Generate.defUseIpAdapter
        self.useIpAdapterPlus = Generate.defUseIpAdapterPlus
        self.useIpAdapterPlusFace = Generate.defUseIpAdapterPlusFace
        self.ipAdapterScale = Generate.defIpAdapterScale
        self.ipAdapterImageDir = Generate.defIpAdapterImageDir

    def getUpscale1Height(self):
        return int(self.height * self.upscale1Scale)

    def getUpscale1Width(self):
        return int(self.width * self.upscale1Scale)

    def getUpscale2Height(self):
        return int(self.getUpscale1Height() * self.upscale2Scale)

    def getUpscale2Width(self):
        return int(self.getUpscale1Width() * self.upscale2Scale)

    @classmethod
    def loadConfig(cls):
        Generate.defLength = Config.getInt("default", "length", Generate.defLength)
        Generate.defModel = Config.get("default", "model", Generate.defModel)
        Generate.defVae = Config.get("default", "vae", Generate.defVae)
        Generate.defMotionModule = Config.get(
            "default", "motion_module", Generate.defMotionModule
        )
        Generate.defContext = Config.getInt("default", "context", Generate.defContext)
        Generate.defScheduler = Config.get(
            "default", "scheduler", Generate.defScheduler
        )
        Generate.defSteps = Config.getInt("default", "steps", Generate.defSteps)
        Generate.defGuidanceScale = Config.getFloat(
            "default", "guidance_scale", Generate.defGuidanceScale
        )
        Generate.defClipSkip = Config.getInt(
            "default", "clip_skip", Generate.defClipSkip
        )
        Generate.defPromptFixedRatio = Config.getFloat(
            "default", "prompt_fixed_ratio", Generate.defPromptFixedRatio
        )
        Generate.defUseHalfVae = Config.getBool(
            "default", "use_half_vae", Generate.defUseHalfVae
        )
        Generate.defUseXFormers = Config.getBool(
            "default", "use_x_formers", Generate.defUseXFormers
        )
        Generate.defWidth = Config.getInt("default", "width", Generate.defWidth)
        Generate.defHeight = Config.getInt("default", "height", Generate.defHeight)
        Generate.defSeed = Config.getInt("default", "seed", Generate.defSeed)

        Generate.defUpscale1Enabled = Config.getBool(
            "default", "upscale1_enabled", Generate.defUpscale1Enabled
        )
        Generate.defUpscale1Mode = Config.get(
            "default", "upscale1_mode", Generate.defUpscale1Mode
        )
        Generate.defUpscale1Scale = Config.getFloat(
            "default", "upscale1_scale", Generate.defUpscale1Scale
        )

        Generate.defUpscale2Enabled = Config.getBool(
            "default", "upscale2_enabled", Generate.defUpscale2Enabled
        )
        Generate.defUpscale2Mode = Config.get(
            "default", "upscale2_mode", Generate.defUpscale2Mode
        )
        Generate.defUpscale2Scale = Config.getFloat(
            "default", "upscale2_scale", Generate.defUpscale2Scale
        )

        Generate.defUpscaleScheduler = Config.get(
            "default", "upscale_scheduler", Generate.defUpscaleScheduler
        )
        Generate.defUpscaleSteps = Config.getInt(
            "default", "upscale_steps", Generate.defUpscaleSteps
        )
        Generate.defUpscaleGuidanceScale = Config.getFloat(
            "default", "upscale_guidance_scale", Generate.defUpscaleGuidanceScale
        )
        Generate.defUpscaleStrength = Config.getFloat(
            "default", "upscale_strength", Generate.defUpscaleStrength
        )
        Generate.defUpscaleTileScale = Config.getFloat(
            "default", "upscale_tile_scale", Generate.defUpscaleTileScale
        )
        Generate.defUpscaleTileStart = Config.getFloat(
            "default", "upscale_tile_start", Generate.defUpscaleTileStart
        )
        Generate.defUpscaleTileEnd = Config.getFloat(
            "default", "upscale_tile_end", Generate.defUpscaleTileEnd
        )
        Generate.defUpscaleUseHalfVae = Config.getBool(
            "default", "upscale_use_half_vae", Generate.defUpscaleUseHalfVae
        )
        Generate.defUpscaleUseXFormers = Config.getBool(
            "default", "upscale_use_x_formers", Generate.defUpscaleUseXFormers
        )

        Generate.defUseIpAdapter = Config.getBool(
            "default", "use_ip_adapter", Generate.defUseIpAdapter
        )
        Generate.defUseIpAdapterPlus = Config.getBool(
            "default", "use_ip_adapter_plus", Generate.defUseIpAdapterPlus
        )
        Generate.defUseIpAdapterPlusFace = Config.getBool(
            "default", "use_ip_adapter_plus_face", Generate.defUseIpAdapterPlusFace
        )
        Generate.defIpAdapterScale = Config.getFloat(
            "default", "ip_adapter_scale", Generate.defIpAdapterScale
        )
        Generate.defIpAdapterImageDir = Config.get(
            "default", "ip_adapter_image_dir", Generate.defIpAdapterImageDir
        )

    def storeConfig(self):
        Config.set("default", "length", Generate.defLength)
        Config.set("default", "model", Generate.defModel)
        Config.set("default", "vae", Generate.defVae)
        Config.set("default", "motion_module", Generate.defMotionModule)
        Config.set("default", "context", Generate.defContext)
        Config.set("default", "scheduler", Generate.defScheduler)
        Config.set("default", "steps", Generate.defSteps)
        Config.set("default", "guidance_scale", Generate.defGuidanceScale)
        Config.set("default", "clip_skip", Generate.defClipSkip)
        Config.set("default", "prompt_fixed_ratio", Generate.defPromptFixedRatio)
        Config.set("default", "use_half_vae", Generate.defUseHalfVae)
        Config.set("default", "use_x_formers", Generate.defUseXFormers)
        Config.set("default", "width", Generate.defWidth)
        Config.set("default", "height", Generate.defHeight)
        Config.set("default", "seed", Generate.defSeed)

        Config.set("default", "upscale1_enabled", Generate.defUpscale1Enabled)
        Config.set("default", "upscale1_mode", Generate.defUpscale1Mode)
        Config.set("default", "upscale1_scale", Generate.defUpscale1Scale)

        Config.set("default", "upscale2_enabled", Generate.defUpscale2Enabled)
        Config.set("default", "upscale2_mode", Generate.defUpscale2Mode)
        Config.set("default", "upscale2_scale", Generate.defUpscale2Scale)

        Config.set("default", "upscale_scheduler", Generate.defUpscaleScheduler)
        Config.set("default", "upscale_steps", Generate.defUpscaleSteps)
        Config.set(
            "default", "upscale_guidance_scale", Generate.defUpscaleGuidanceScale
        )
        Config.set("default", "upscale_strength", Generate.defUpscaleStrength)
        Config.set("default", "upscale_tile_scale", Generate.defUpscaleTileScale)
        Config.set("default", "upscale_tile_start", Generate.defUpscaleTileStart)
        Config.set("default", "upscale_tile_end", Generate.defUpscaleTileEnd)
        Config.set("default", "upscale_use_half_vae", Generate.defUpscaleUseHalfVae)
        Config.set("default", "upscale_use_x_formers", Generate.defUpscaleUseXFormers)

        Config.set("default", "use_ip_adapter", Generate.defUseIpAdapter)
        Config.set("default", "use_ip_adapter_plus", Generate.defUseIpAdapterPlus)
        Config.set(
            "default", "use_ip_adapter_plus_face", Generate.defUseIpAdapterPlusFace
        )
        Config.set("default", "ip_adapter_scale", Generate.defIpAdapterScale)
        Config.set("default", "ip_adapter_image_dir", Generate.defIpAdapterImageDir)

editor/src/mdl_generate.py
Zuntan03/EasyPromptAnime
﻿class Notifier:
    def __init__(self):
        self._subscribers = {}
        pass

    def subsc(self, memberName, func):
        if self._subscribers.get(memberName) is None:
            self._subscribers[memberName] = []
        self._subscribers[memberName].append(func)

    def unsubsc(self, memberName, func):
        if self._subscribers.get(memberName) is None:
            return
        self._subscribers[memberName].remove(func)
        if len(self._subscribers[memberName]) == 0:
            del self._subscribers[memberName]

    def notify(self, memberName, value):
        if self._subscribers.get(memberName) is None:
            return
        for func in self._subscribers[memberName]:
            func(self, memberName, value)

    def notifyAll(self):
        for memberName in self._subscribers.keys():
            self.notify(memberName, getattr(self, memberName))

    def set(self, memberName, value):
        if getattr(self, memberName) == value:
            return
        setattr(self, memberName, value)
        self.notify(memberName, value)

    def trace_add(self, memberName, var):
        return var.trace_add("write", lambda *args: self.set(memberName, var.get()))

    def trace_remove(self, var, tid):
        var.trace_remove("write", tid)

    def bind(self, memberName, var):
        self.subsc(memberName, lambda *args: var.set(getattr(self, memberName)))
        self.trace_add(memberName, var)

editor/src/mdl_notifier.py
Zuntan03/EasyPromptAnime
﻿from const import Path
from mdl_notifier import Notifier
import os
from prompt_perser import PromptPerser


class Prompt(Notifier):
    defaultPrompt = """H: crowds, akihabara
 0: standing, upset
10: waving at viewer, surprised
20: waving at viewer, smile
F: 1girl, maid outfit
N:(worst quality, low quality:1.4)
"""

    def __init__(self):
        super().__init__()
        self.original = ""
        self.prompts = {
            "header": "",
            "footer": "",
            "negative": "",
            "lora_map": {},
            "prompt_map": {},
            "errors": [],
        }
        self.subsc("original", self.updatePrompts)

    def updatePrompts(self, *args):
        self.prompts = PromptPerser.persePrompt(self.original)

    def getPreview(self, stert, length, showKeyframe, showHeaderFooter, showAnime):
        return PromptPerser.persePreview(
            self.prompts, stert, length, showKeyframe, showHeaderFooter, showAnime
        )

    def loadDefaultPrompt(self):
        if os.path.exists(Path.defaultPrompt):
            with open(Path.defaultPrompt, "r", encoding="utf-8-sig") as f:
                self.set("original", f.read())
        else:
            with open(Path.defaultPrompt, "w", encoding="utf-8-sig") as f:
                f.write(Prompt.defaultPrompt)
            self.set("original", Prompt.defaultPrompt)

    def getLastPrompt(self):
        frames = sorted(self.prompts["prompt_map"].keys())
        length = len(frames)
        if length == 0:
            return (0, "")
        maxFrame = frames[length - 1]
        return (maxFrame, self.prompts["prompt_map"][maxFrame])

editor/src/mdl_prompt.py
Zuntan03/EasyPromptAnime
﻿from mdl_prompt import Prompt
from mdl_generate import Generate
from mdl_editor import Editor


class Model:
    def __init__(self):
        self.prompt = Prompt()
        self.generate = Generate()
        self.editor = Editor()

    def notifyAll(self):
        self.prompt.notifyAll()
        self.generate.notifyAll()
        self.editor.notifyAll()

    @classmethod
    def loadConfig(cls):
        Generate.loadConfig()
        Editor.loadConfig()

    def storeConfig(self):
        self.generate.storeConfig()
        self.editor.storeConfig()

    def getPreviewLength(self):
        if self.editor.previewLength == 0:
            return self.generate.length - self.editor.previewStart
        else:
            return self.editor.previewLength

    def getPreviewEnd(self):
        if self.editor.previewLength == 0:
            return self.generate.length
        else:
            return self.editor.previewStart + self.editor.previewLength

editor/src/model.py
Zuntan03/EasyPromptAnime
﻿from l10n import L10n
from collections import OrderedDict


class PromptPerser:
    @classmethod
    def persePrompt(cls, txt):
        prompts = {
            "header": "",
            "footer": "",
            "negative": "",
            "lora_map": {},
            "prompt_map": {},
            "errors": [],
        }

        for line in txt.splitlines():
            try:
                cls.perseLine(prompts, line)
            except Exception as e:
                prompts["errors"].append(str(e))

        # TODO: apply range prompt
        return prompts

    @classmethod
    def perseLine(cls, prompts, line):
        commentRemoved = line.split("#", 1)[0].strip()
        if commentRemoved == "":
            return
        commentRemovedKvp = commentRemoved.split(":", 1)
        stripedKey = commentRemovedKvp[0].strip()
        if len(commentRemovedKvp) < 2:
            cls.addPrompt(prompts, 0, stripedKey)  # 0 frame prompt
            return
        val = commentRemovedKvp[1].strip()
        if stripedKey == "":
            cls.addPrompt(prompts, 0, val)  # 0 frame prompt
            return
        key = stripedKey.lower()
        if key == "h":
            if prompts["header"] != "":
                prompts["header"] += ", "
            prompts["header"] += val
        elif key == "f":
            if prompts["footer"] != "":
                prompts["footer"] += ", "
            prompts["footer"] += val
        elif key == "n":
            if prompts["negative"] != "":
                prompts["negative"] += ", "
            prompts["negative"] += val
        elif key == "l":
            for lora_def in val.split(","):
                lora_kvp = lora_def.split(":", 1)
                if len(lora_kvp) < 2:
                    prompts["errors"].append(
                        L10n.format("err_inv_lora_def", lora_def, val)
                    )
                    return
                lora_key = lora_kvp[0].strip()
                lora_weight = lora_kvp[1].strip()
                try:
                    lora_weight = float(lora_weight)
                    prompts["lora_map"][lora_key] = lora_weight
                except ValueError:
                    prompts["errors"].append(
                        L10n.format("err_inv_lora_def", lora_def, val)
                    )
                    return
        elif key.isdigit():
            cls.addPrompt(prompts, int(key), val)
        else:
            # TODO: parse range prompt
            prompts["errors"].append(L10n.format("err_inv_line", line))

    @classmethod
    def addPrompt(self, prompts, frame, prompt):
        if not isinstance(frame, int) or frame < 0:
            prompts["errors"].append(L10n.format("err_inv_frame_num", frame, prompt))
            return

        promptMap = prompts["prompt_map"]
        if frame in promptMap:
            promptMap[frame] = f"{promptMap[frame]}, {prompt}"
        else:
            promptMap[frame] = prompt

    @classmethod
    def persePreview(
        cls, prompts, start, length, showKeyframe, showHeaderFooter, showAnime
    ):
        preview = []
        header = prompts["header"]
        header = (header + ", ") if header != "" else header

        footer = prompts["footer"]
        footer = (", " + footer) if footer != "" else footer

        end = start + length
        firstPrompt = ""
        preFrame = -1
        prePrompt = ""
        for frame in range(start, end + 1):
            if frame in prompts["prompt_map"]:
                prompt = prompts["prompt_map"][frame]
                if firstPrompt == "":
                    firstPrompt = prompt
                else:
                    if showAnime:
                        preview.append(
                            f"{preFrame:>3}-{frame:>2}: ({cls.diffPrompt(prePrompt, prompt)}: 0.5)"
                        )
                if showKeyframe:
                    if showHeaderFooter:
                        preview.append(f"{frame:>3}: {header}{prompt}{footer}")
                    else:
                        preview.append(f"{frame:>3}: {prompt}")
                preFrame = frame
                prePrompt = prompt
        if firstPrompt != "" and showAnime:
            preview.append(
                f"{preFrame:>3}-{start:>2}: ({cls.diffPrompt(prePrompt, firstPrompt)}: 0.5)"
            )

        negative = prompts["negative"]
        if negative != "":
            preview.append(f"{L10n.get('negative_prompt')}: {negative}")

        loras = prompts["lora_map"].keys()
        if len(loras) > 0:
            loraDefs = "LoRA: "
            for lora in loras:
                loraDefs += f"<{lora}: {prompts['lora_map'][lora]}> "
            preview.append(loraDefs)

        for err in prompts["errors"]:
            preview.append(f"{L10n.get('error')}: {err}")

        return "\n".join(preview)

    @classmethod
    def diffPrompt(cls, prePrompt, postPrompt):
        preSet = set(map(str.strip, prePrompt.split(",")))
        postSet = set(map(str.strip, postPrompt.split(",")))
        uniqueSet = preSet.symmetric_difference(postSet)
        return ", ".join(uniqueSet)  # + f" | {prePrompt} | {postPrompt}"

editor/src/prompt_perser.py
Zuntan03/EasyPromptAnime
﻿import os
from const import Const, Path
from l10n import L10n
from collections import OrderedDict


class PromptTravel:
    defaultTemplate = """{{
    "name": "",
    "path": "{MODEL_PATH}",
    "vae_path": "{VAE_PATH}",
    "motion_module": "{MOTION_MODULE_PATH}",
    "compile": false,
    "seed": [{SEED}],
    "scheduler": "{SCHEDULER}",
    "steps": {STEPS},
    "guidance_scale": {GUIDANCE_SCALE},
    "clip_skip": {CLIP_SKIP},
    "prompt_fixed_ratio": {PROMPT_FIXED_RATIO},
    "head_prompt": "{HEADER_PROMPT}",
    "prompt_map": {{
{PROMPT_MAP}
    }},
    "tail_prompt": "{FOOTER_PROMPT}",
    "n_prompt": ["{NEGATIVE_PROMPT}"],
    "lora_map": {{
{LORA_MAP}
    }},
    "ip_adapter_map": {{
        "enable": {IP_ADAPTER_USE},
        "input_image_dir": "ip_adapter_image/{IP_ADAPTER_IMAGE_DIR}",
        "save_input_image": true,
        "scale": {IP_ADAPTER_SCALE},
        "is_plus_face": {IP_ADAPTER_USE_PLUS_FACE},
        "is_plus": {IP_ADAPTER_USE_PLUS}
    }},
    "upscale_config": {{
        "scheduler": "{UPSCALE_SCHEDULER}",
        "steps": {UPSCALE_STEPS},
        "strength": {UPSCALE_STRENGTH},
        "guidance_scale": {UPSCALE_GUIDANCE_SCALE},
        "controlnet_tile": {{
            "enable": true,
            "use_preprocessor": false,
            "controlnet_conditioning_scale": {UPSACLE_TILE_SCALE},
            "guess_mode": false,
            "control_guidance_start": {UPSACLE_TILE_START},
            "control_guidance_end": {UPSACLE_TILE_END}
        }}
    }},
    "output": {{
        "format": "mp4",
        "fps": {FPS},
        "encode_param": {{
            "crf": 20
        }}
    }}
}}
"""

    modelDir = os.path.join("models", "sd")
    motionModuleDir = os.path.join("models", "motion-module")
    vaeDir = "vae"

    @classmethod
    def loadTemplate(cls):
        if os.path.exists(Path.promptTravelTemplate):
            with open(Path.promptTravelTemplate, "r", encoding="utf-8-sig") as f:
                return f.read()
        else:
            with open(Path.promptTravelTemplate, "w", encoding="utf-8-sig") as f:
                f.write(PromptTravel.defaultTemplate)
            return PromptTravel.defaultTemplate

    @classmethod
    def getConfig(cls, generate, model, upscaleStrength, prompts, fps):
        modelPath = os.path.join(cls.modelDir, model).replace("\\", "/")

        motionModulePath = os.path.join(cls.motionModuleDir, generate.motionModule)
        motionModulePath = motionModulePath.replace("\\", "/")

        vaePath = os.path.join(cls.vaeDir, generate.vae).replace("\\", "/")

        promptMap = ""
        for frame, prompt in prompts["prompt_map"].items():
            if promptMap != "":
                promptMap += ",\n"
            promptMap += f'        "{frame}": "{prompt}"'

        loraMap = ""
        for loraName, loraWeight in prompts["lora_map"].items():
            if loraMap != "":
                loraMap += ",\n"
            loraMap += f'        "lora/{loraName}.safetensors": {loraWeight}'

        replaceDic = {
            "MODEL_PATH": modelPath,
            "VAE_PATH": vaePath,
            "MOTION_MODULE_PATH": motionModulePath,
            "SEED": generate.seed,
            "SCHEDULER": Const.getSchedulerValue(generate.scheduler),
            "STEPS": generate.steps,
            "GUIDANCE_SCALE": generate.guidanceScale,
            "CLIP_SKIP": generate.clipSkip,
            "PROMPT_FIXED_RATIO": generate.promptFixedRatio,
            "HEADER_PROMPT": prompts["header"],
            "PROMPT_MAP": promptMap,
            "FOOTER_PROMPT": prompts["footer"],
            "NEGATIVE_PROMPT": prompts["negative"],
            "LORA_MAP": loraMap,
            "UPSCALE_SCHEDULER": Const.getSchedulerValue(generate.upscaleScheduler),
            "UPSCALE_STEPS": generate.upscaleSteps,
            "UPSCALE_STRENGTH": upscaleStrength,
            "UPSCALE_GUIDANCE_SCALE": generate.upscaleGuidanceScale,
            "UPSACLE_TILE_SCALE": generate.upscaleTileScale,
            "UPSACLE_TILE_START": generate.upscaleTileStart,
            "UPSACLE_TILE_END": generate.upscaleTileEnd,
            "IP_ADAPTER_USE": str(generate.useIpAdapter).lower(),
            "IP_ADAPTER_IMAGE_DIR": generate.ipAdapterImageDir,
            "IP_ADAPTER_SCALE": generate.ipAdapterScale,
            "IP_ADAPTER_USE_PLUS_FACE": str(generate.useIpAdapterPlusFace).lower(),
            "IP_ADAPTER_USE_PLUS": str(generate.useIpAdapterPlus).lower(),
            "FPS": fps,
        }

        return PromptTravel.loadTemplate().format(**replaceDic)

    @classmethod
    def GetConfigFileSuffix(
        cls,
        length,
        context,
        width,
        height,
        useHalfVae,
        useXFormers,
        upscaleUseHalfVae,
        upscaleUseXFormers,
        upscale1Enabled=False,
        upscale1Mode=Const.tile,
        upscale1Height=0,
        upscale2Enabled=False,
        upscale2Mode=Const.tile,
        upscale2Height=0,
    ):
        fileSuffix = f"-L{length}-C{context}-W{width}-H{height}"
        fileSuffix += "-v" if useHalfVae else ""
        fileSuffix += "-x" if useXFormers else ""
        fileSuffix += "-V" if upscaleUseHalfVae else ""
        fileSuffix += "-X" if upscaleUseXFormers else ""
        if upscale1Enabled:
            if upscale1Mode == Const.tile:
                fileSuffix += f"-T{upscale1Height}"
            elif upscale1Mode == Const.refine:
                fileSuffix += f"-R{upscale1Height}"

            if upscale2Enabled:
                if upscale2Mode == Const.tile:
                    fileSuffix += f"-T{upscale2Height}"
                elif upscale2Mode == Const.refine:
                    fileSuffix += f"-R{upscale2Height}"
        return fileSuffix

editor/src/prompt_travel.py
Zuntan03/EasyPromptAnime
﻿import time
from l10n import L10n
from log import Log
from const import Path


class Task:
    win = None
    queue = []
    queueInfoChangedEvent = []
    pollTime = 100

    @classmethod
    def initialize(cls, win):
        Task.win = win

    @classmethod
    def queueInfoChanged(cls):
        [handler(cls.queue) for handler in cls.queueInfoChangedEvent]

    @classmethod
    def enqueue(cls, task):
        task.resetState()
        cls.queue.append(task)
        cls.queueInfoChanged()
        if len(cls.queue) == 1:
            cls.win.after(cls.pollTime, cls.runQueue)

    @classmethod
    def dequeue(cls):
        task = cls.queue[0]
        cls.queue.remove(task)
        cls.queueInfoChanged()
        return task

    @classmethod
    def runQueue(cls):
        if len(cls.queue) == 0:
            return

        if cls.queue[0].isComleted:
            endTask = cls.dequeue()
            endTask.time = time.perf_counter() - endTask.startTime
            endTask.logEnd()

            if endTask.reRun():
                cls.enqueue(endTask)
            if len(cls.queue) == 0:
                return

        startNow = not cls.queue[0].isRunning
        if startNow:
            cls.queue[0].startTime = time.perf_counter()
            cls.queue[0].timeStr = Path.getYYYYMMDDHHMMSS()
            cls.queue[0].isRunning = True
            cls.queue[0].logStart()

        if cls.queue[0].run() or startNow:
            cls.queueInfoChanged()

        if len(cls.queue) > 0:
            cls.win.after(cls.pollTime, cls.runQueue)

    def __init__(self):
        self.resetState()

    def resetState(self):
        self.startTime = 0.0
        self.time = 0.0
        self.timeStr = ""
        self.isRunning = False
        self.isComleted = False
        self.isFailed = False

    def run(self):
        return False

    def reRun(self):
        return False

    def logStart(self):
        Log.user(L10n.format("log_task_start", self))

    def logEnd(self):
        if self.isFailed:
            Log.user(L10n.format("log_task_failed", self, self.time))
        else:
            Log.user(L10n.format("log_task_success", self, self.time))

    def __str__(self):
        if self.isComleted:
            if self.isFailed:
                return "[F]"
            else:
                return "[S]"
        else:
            if self.isRunning:
                return "[R]"
            else:
                return "[_]"

editor/src/task.py
Zuntan03/EasyPromptAnime
﻿import os, subprocess, threading
from const import Path
from l10n import L10n
from task import Task


class CommandPromptTask(Task):
    def __init__(self, editor, taskMode):
        super().__init__()
        self.editor = editor
        self.taskMode = taskMode
        self.taskName = L10n.get(taskMode)

    def resetState(self):
        super().resetState()
        self.tmpDir = ""
        self.errorPath = ""

        self.command = None
        self.process = None
        self.log = ""
        self.thread = None

    def run(self):
        if self.thread is None:
            self.startThread()
        return self.pollThread()

    def startThread(self):
        self.tmpDir = os.path.join(Path.tmp, self.timeStr)
        os.path.exists(self.tmpDir) or os.makedirs(self.tmpDir)

        self.errorPath = os.path.join(self.tmpDir, f"{self.timeStr}-Error.txt")

        cmd = self.createCommand()
        if self.editor.taskPauseByError:
            cmd += f" || (echo %errorlevel% > {self.errorPath} & pause)"
        else:
            cmd += f" || (echo %errorlevel% > {self.errorPath})"
        self.command = ["start", "/wait", "cmd", "/c", cmd]

        self.log = ""
        self.process = subprocess.Popen(
            self.command,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            shell=True,
            encoding="utf-8",
        )
        self.thread = threading.Thread(target=self.threadMain)
        self.thread.start()

    def createCommand(self):
        return ""

    def threadMain(self):
        self.log = self.process.communicate()[0]

    def pollThread(self):
        if self.thread.is_alive():
            return False

        self.isComleted = True
        self.isFailed = (
            (self.process.returncode != 0)
            or (os.path.exists(self.errorPath))
            or (self.log == "^C")
        )

        self.thread = None
        self.process = None
        self.isFailed or self.postProcess()
        # Log.system(self.log) # "^C" or ""
        return True

    def postProcess(self):
        pass

    def reRun(self):
        if self.isFailed:
            return False
        return self.editor.taskForever

    def __str__(self):
        return f"{super().__str__()} {self.taskName} {self.timeStr}"

editor/src/tsk_command_prompt.py
Zuntan03/EasyPromptAnime
﻿import os, copy, shutil, subprocess
from const import Path
from l10n import L10n
from log import Log
from tsk_command_prompt import CommandPromptTask
from prompt_travel import PromptTravel


class GenerateTask(CommandPromptTask):
    @classmethod
    def generate(cls, model):
        gen = model.generate
        config = PromptTravel.getConfig(
            gen, gen.model, gen.upscaleStrength, model.prompt.prompts, 10
        )
        configSuffix = PromptTravel.GetConfigFileSuffix(
            gen.length,
            gen.context,
            gen.width,
            gen.height,
            gen.useHalfVae,
            gen.useXFormers,
            gen.upscaleUseHalfVae,
            gen.upscaleUseXFormers,
            gen.upscale1Enabled,
            gen.upscale1Mode,
            gen.getUpscale1Height(),
            gen.upscale2Enabled,
            gen.upscale2Mode,
            gen.getUpscale2Height(),
        )
        configSuffix = configSuffix + "-D3"
        task = GenerateTask(model.editor, "generate_anime", config, configSuffix)
        cls.enqueue(task)

    @classmethod
    def seedGacha(cls, model):
        gen = model.generate
        config = PromptTravel.getConfig(
            gen, gen.model, gen.upscaleStrength, model.prompt.prompts, 10
        )
        configSuffix = PromptTravel.GetConfigFileSuffix(
            gen.length,
            gen.context,
            gen.width,
            gen.height,
            gen.useHalfVae,
            gen.useXFormers,
            gen.upscaleUseHalfVae,
            gen.upscaleUseXFormers,
            gen.upscale1Enabled,
            gen.upscale1Mode,
            (int)(gen.height * 1.5),
        )
        task = GenerateTask(model.editor, "seed_gacha", config, configSuffix)
        cls.enqueue(task)

    @classmethod
    def preview(cls, model):
        halfFps = False  # Not good

        edt = model.editor
        start = edt.previewStart
        length = model.getPreviewLength()
        end = model.getPreviewEnd()
        prompts = copy.deepcopy(model.prompt.prompts)
        promptMap = prompts["prompt_map"]

        removeKeys = []
        for keyFrame in promptMap.keys():
            if keyFrame < start or keyFrame > end:
                removeKeys.append(keyFrame)
        for keyFrame in removeKeys:
            del promptMap[keyFrame]

        for keyFrame in sorted(promptMap.keys()):
            prompt = promptMap.pop(keyFrame)
            frame = keyFrame - start
            frame = (int)(frame * 0.5) if halfFps else frame
            promptMap[frame] = prompt

        gen = model.generate
        config = PromptTravel.getConfig(
            gen, gen.model, gen.upscaleStrength, prompts, 5 if halfFps else 10
        )
        configSuffix = PromptTravel.GetConfigFileSuffix(
            (int)(length * 0.5) if halfFps else length,
            (int)(gen.context * 0.5)
            if halfFps
            else gen.context,  # halfFps & gen.context Not good
            gen.width,
            gen.height,
            gen.useHalfVae,
            gen.useXFormers,
            gen.upscaleUseHalfVae,
            gen.upscaleUseXFormers,
            gen.upscale1Enabled and edt.previewUpscale,
            gen.upscale1Mode,
            (int)(gen.height * 1.5),
        )
        configSuffix = (configSuffix + "-U5") if halfFps else configSuffix
        task = GenerateTask(model.editor, "preview", config, configSuffix)
        cls.enqueue(task)

    # コンストラクタ
    def __init__(self, editor, taskMode, config, configSuffix):
        super().__init__(editor, taskMode)
        self.config = config
        self.configSuffix = configSuffix

    def resetState(self):
        super().resetState()
        self.configName = ""
        self.configPath = ""

    def createCommand(self):
        self.configName = f"{self.timeStr}{self.configSuffix}.json"
        self.configPath = os.path.join(self.tmpDir, self.configName)
        with open(self.configPath, "w", encoding="utf-8") as f:
            f.write(self.config)
        return f"Generate.bat {self.configPath}"

    def postProcess(self):
        latestTime = 0
        latestMp4FileName = ""
        latestMp4Path = ""

        for root, dirs, files in os.walk(self.tmpDir):
            for file in files:
                if file.endswith(".mp4"):
                    path = os.path.join(root, file)
                    time = os.path.getmtime(path)
                    if time > latestTime:
                        latestTime = time
                        latestMp4FileName = file
                        latestMp4Path = path

        outputDir = os.path.join(Path.output, Path.getYYYYMMDD())
        os.makedirs(outputDir, exist_ok=True)
        outputPath = os.path.join(outputDir, latestMp4FileName)
        shutil.copy(latestMp4Path, outputPath)

        seed = ""
        tokens = latestMp4FileName.split("-", 2)
        if len(tokens) == 3:
            seed = tokens[1]
        seed = seed.split("_", 1)[0]

        subprocess.Popen(f"{self.editor.ffplayCmd} {outputPath}", shell=True)
        Log.user(L10n.format("log_ffplay", seed))

editor/src/tsk_generate.py
Zuntan03/EasyPromptAnime
﻿import os, glob, shutil, subprocess
from const import Const, Path
from l10n import L10n
from log import Log
from tsk_command_prompt import CommandPromptTask
from prompt_travel import PromptTravel


class UpscaleTask(CommandPromptTask):
    @classmethod
    def upscaleWithConfigOverride(cls, model, upscaleDir):
        gen = model.generate
        config = PromptTravel.getConfig(
            gen, gen.model, gen.upscaleStrength, model.prompt.prompts, 10
        )
        cls.upscale(model, upscaleDir, config)

    @classmethod
    def upscale(cls, model, upscaleDir, config=None):
        gen = model.generate
        task = UpscaleTask(
            model.editor,
            "upscale",
            upscaleDir,
            gen.upscale1Mode,
            gen.getUpscale1Height(),
            gen.context,
            gen.upscaleUseHalfVae,
            gen.upscaleUseXFormers,
            config,
        )
        cls.enqueue(task)

    def __init__(
        self,
        editor,
        taskMode,
        upscaleDir,
        mode,
        height,
        context,
        useHalfVae,
        useXFormers,
        config,
    ):
        super().__init__(editor, taskMode)
        self.upscaleDir = upscaleDir
        self.mode = mode
        self.height = height
        self.context = context
        self.useHalfVae = useHalfVae
        self.useXFormers = useXFormers
        self.config = config

    def resetState(self):
        super().resetState()
        self.configName = ""
        self.configPath = ""

    def createCommand(self):
        cmd = os.path.join("editor", "bat", self.mode + ".bat")
        cmd = f"{cmd} -H {self.height}"
        if self.mode == Const.refine:
            cmd += f" -C {(int)(self.context / 2)}"
        if self.useHalfVae:
            cmd += " --half-vae"
        if self.useXFormers:
            cmd += " --xformers"

        if self.config is not None:
            self.configName = f"{self.timeStr}.json"
            configDir = os.path.abspath(os.path.join(self.upscaleDir, os.pardir))
            self.configPath = os.path.join(configDir, self.configName)
            with open(self.configPath, "w", encoding="utf-8") as f:
                f.write(self.config)
            cmd += f" -c {self.configPath}"  # dont ""
        cmd += f" {self.upscaleDir}"  # dont ""
        return cmd

    def postProcess(self):
        outputDir = ""
        if self.mode == Const.tile:
            outputDir = self.findNewDir(Path.promptTravelUpscaled)
        elif self.mode == Const.refine:
            outputDir = self.findNewDir(Path.promptTravelRefined)
            if outputDir is not None:
                outputDir = self.findNewDir(outputDir)
        if outputDir is None:
            return
        mp4Paths = glob.glob(os.path.join(outputDir, "*.mp4"))
        if len(mp4Paths) == 0:
            return
        mp4Path = max(mp4Paths, key=os.path.getmtime)
        mp4Name = os.path.basename(mp4Path)

        outputDir = os.path.join(Path.output, Path.getYYYYMMDD())
        os.makedirs(outputDir, exist_ok=True)
        outputPath = os.path.join(outputDir, mp4Name)
        shutil.copy(mp4Path, outputPath)

        seed = ""
        tokens = mp4Name.split(r"[_-]", 2)
        if len(tokens) == 3:
            seed = tokens[1]

        subprocess.Popen(f"{self.editor.ffplayCmd} {outputPath}", shell=True)
        Log.user(L10n.format("log_ffplay", seed))

    def findNewDir(self, dirPath):
        subDirs = [
            os.path.join(dirPath, name)
            for name in os.listdir(dirPath)
            if os.path.isdir(os.path.join(dirPath, name))
        ]
        newDir = max(subDirs, key=lambda x: os.path.getmtime(x), default=None)  # None
        return newDir

editor/src/tsk_upscale.py
Zuntan03/EasyPromptAnime
﻿from config import Config
from l10n import L10n
from ui_menu import Menu
from ui_input import InputForm
from ui_preview import PreviewForm
from ui_basic import BasicForm
from ui_generate import GenerateForm
from ui_upscale import UpscaleForm
from ui_ip_adapter import IpAdapterForm
from ui_output import OutputForm
import tkinter as tk
import tkinter.ttk as ttk
import darkdetect


class Form:
    winMinW = 600
    winMinH = 750
    winInitW = 800
    winInitH = 900
    winInitX = "None"
    winInitY = "40"

    ntbFuncInitH = 400
    ntbFuncMinH = 400

    def __init__(self):
        self.win = tk.Tk()
        self.win.title(f'{L10n.get("title")}: {L10n.get("news")}')
        self.win.minsize(Form.winMinW, Form.winMinH)

        winGeom = f"{Form.winInitW}x{Form.winInitH}"
        if Form.winInitX != "None":
            winGeom += f"+{Form.winInitX}+{Form.winInitY}"
        self.win.geometry(winGeom)

        self.menu = Menu(self.win)

        self.paneWin = tk.PanedWindow(self.win, orient=tk.VERTICAL, sashpad=2)
        self.input = InputForm(self.paneWin)

        self.ntbFunc = ttk.Notebook(self.paneWin)
        self.preview = PreviewForm(self.ntbFunc)
        self.basic = BasicForm(self.ntbFunc)
        self.generate = GenerateForm(self.ntbFunc)
        self.upscale = UpscaleForm(self.ntbFunc)
        self.ipAdapter = IpAdapterForm(self.ntbFunc)
        self.paneWin.add(
            self.ntbFunc,
            height=self.ntbFuncH,
            minsize=Form.ntbFuncMinH,
            stretch="never",
        )
        self.ntbFunc.select(1)  # Default 1

        self.output = OutputForm(self.paneWin)
        self.paneWin.pack(fill=tk.BOTH, expand=True)

        if Form.invOsCol ^ darkdetect.isDark():
            self.setDarkTheme()

    @classmethod
    def loadConfig(cls):
        Form.winInitW = Config.get("ui_size", "win_width", Form.winInitW)
        Form.winInitH = Config.get("ui_size", "win_height", Form.winInitH)
        Form.winInitX = Config.get("ui_size", "win_x", Form.winInitX)
        Form.winInitY = Config.get("ui_size", "win_y", Form.winInitY)

        InputForm.loadConfig()
        Form.ntbFuncH = Config.get("ui_size", "func_h", Form.ntbFuncInitH)
        BasicForm.loadConfig()
        GenerateForm.loadConfig()
        UpscaleForm.loadConfig()
        OutputForm.loadConfig()

        Form.darkColFg = Config.get("ui_color", "dark_col_fg", "#CCCCCC")
        Form.darkColFgSel = Config.get("ui_color", "dark_col_fg_sel", "#FFFFFF")
        Form.darkColBg = Config.get("ui_color", "dark_col_bg", "#222222")
        Form.darkColBgSel = Config.get("ui_color", "dark_col_bg_sel", "#555555")
        Form.invOsCol = Config.getBool("ui_color", "inv_os_col", False)

    def storeConfig(self):
        Config.set("ui_size", "win_width", self.win.winfo_width())
        Config.set("ui_size", "win_height", self.win.winfo_height())
        Config.set("ui_size", "win_x", self.win.winfo_x())
        Config.set("ui_size", "win_y", self.win.winfo_y())

        self.input.storeConfig()
        func_h = self.ntbFunc.winfo_height()
        if func_h != 1:
            Config.set("ui_size", "func_h", func_h)
        self.basic.storeConfig()
        self.generate.storeConfig()
        self.output.storeConfig()

        Config.set("ui_color", "dark_col_fg", Form.darkColFg)
        Config.set("ui_color", "dark_col_fg_sel", Form.darkColFgSel)
        Config.set("ui_color", "dark_col_bg", Form.darkColBg)
        Config.set("ui_color", "dark_col_bg_sel", Form.darkColBgSel)
        Config.set("ui_color", "inv_os_col", Form.invOsCol)

    def setDarkTheme(self):
        colFgBg = {"fg": Form.darkColFg, "bg": Form.darkColBg}
        colSel = colFgBg.copy()
        colSel["selectforeground"] = Form.darkColFgSel
        colSel["selectbackground"] = Form.darkColBgSel
        colSelIns = colSel.copy()
        colSelIns["insertbackground"] = Form.darkColFgSel
        colSelHigh = colSel.copy()
        colSelHigh["highlightbackground"] = Form.darkColBgSel

        colAct = colFgBg.copy()
        colAct["activeforeground"] = Form.darkColFgSel
        colAct["activebackground"] = Form.darkColBgSel
        colActChk = colAct.copy()
        colActChk["selectcolor"] = Form.darkColBgSel

        sli = colFgBg.copy()
        sli["activebackground"] = Form.darkColBgSel
        sli["troughcolor"] = Form.darkColBg
        sli["highlightbackground"] = Form.darkColBg

        colors = {
            "fg": Form.darkColFg,
            "bg": Form.darkColBg,
            "fgSel": Form.darkColFgSel,
            "bgSel": Form.darkColBgSel,
            "fgBg": colFgBg,
            "sel": colSel,
            "selIns": colSelIns,
            "act": colAct,
            "actChk": colActChk,
            "txt": colSelIns,
            "chk": colActChk,
            "ent": colSelIns,
            "sli": sli,
            "lst": colSelHigh,
        }

        style = ttk.Style()
        style.theme_use("default")
        style.configure("TFrame", background=colors["bg"])
        style.configure("TLabel", background=colors["bg"], foreground=colors["fg"])

        style.configure(
            "TButton",
            foreground=colors["fg"],
            background=colors["bg"],
        )
        style.map(
            "TButton",
            foreground=[("active", colors["fgSel"])],
            background=[("active", colors["bgSel"])],
        )

        style.configure(
            "TCombobox",
            foreground=colors["fg"],
        )
        style.map(
            "TCombobox",
            fieldbackground=[("readonly", colors["bg"])],
            selectbackground=[
                ("readonly", colors["bg"]),
                ("readonly", "!focus", colors["bg"]),
            ],
        )

        style.configure("TNotebook", background=colors["bg"])
        style.configure("TNotebook.Tab", padding=[6, 8])
        style.map(
            "TNotebook.Tab",
            foreground=[("", colors["fg"]), ("selected", colors["fgSel"])],
            background=[("", colors["bg"]), ("selected", colors["bgSel"])],
        )

        self.input.setDarkTheme(colors)
        self.preview.setDarkTheme(colors)
        self.basic.setDarkTheme(colors)
        self.generate.setDarkTheme(colors)
        self.upscale.setDarkTheme(colors)
        self.ipAdapter.setDarkTheme(colors)
        self.output.setDarkTheme(colors)

    def run(self):
        self.win.lift()
        self.win.mainloop()

editor/src/ui.py
Zuntan03/EasyPromptAnime
﻿from const import Const, Path
from l10n import L10n
from config import Config
from ui_const import UiPack
import tkinter as tk
import tkinter.ttk as ttk
import os, glob


class BasicForm:
    maxLength = 600
    sizeResolution = 64

    def __init__(self, parent):
        self.frm = ttk.Frame(parent)

        self.initLengthSeed()
        self.initModelVae()
        self.initSize()
        self.initUpsdcale1()
        self.initUpsdcale2()

        parent.add(self.frm, text=L10n.get("basic_tab"))

    def initLengthSeed(self):
        self.frmLengthSeed = ttk.Frame(self.frm)

        self.varLength = tk.IntVar(value=10)
        self.lblLength = ttk.Label(self.frmLengthSeed, text=L10n.get("generate_length"))
        self.varLength.trace_add("write", self.updateLength)
        self.lblLength.pack(UiPack.lbl)
        self.sliLength = tk.Scale(
            self.frmLengthSeed,
            from_=10,
            to=BasicForm.maxLength,
            resolution=10,
            orient=tk.HORIZONTAL,
            variable=self.varLength,
            takefocus=True,
        )
        self.sliLength.pack(UiPack.sli)

        self.lblSeed = ttk.Label(self.frmLengthSeed, text=L10n.get("generate_seed"))
        self.lblSeed.pack(UiPack.lbl)

        self.varSeed = tk.StringVar(value="-1")
        self.entSeed = tk.Entry(self.frmLengthSeed, textvariable=self.varSeed, width=20)
        self.entSeed.pack(UiPack.ent)
        self.btnRandomSeed = ttk.Button(
            self.frmLengthSeed,
            text="🎲",
            command=lambda: self.varSeed.set("-1"),
        )
        self.btnRandomSeed.pack(UiPack.btn)

        self.frmLengthSeed.pack(UiPack.frm)

    def updateLength(self, *args):
        self.lblLength.configure(
            text=L10n.get("generate_length").format(int(self.varLength.get() / 10))
        )

    def initModelVae(self):
        self.frmModelVae = ttk.Frame(self.frm)
        self.lblModel = ttk.Label(self.frmModelVae, text=L10n.get("generate_model"))
        self.lblModel.pack(UiPack.lbl)

        self.varModel = tk.StringVar()
        self.cmbModel = ttk.Combobox(
            self.frmModelVae,
            textvariable=self.varModel,
            state="readonly",
            postcommand=self.listModeValues,
            width=40,
            height=23,
        )
        self.cmbModel.pack(UiPack.cmb, fill=tk.X, expand=True)
        self.listModeValues()

        self.lblVae = ttk.Label(self.frmModelVae, text=L10n.get("generate_vae"))
        self.lblVae.pack(UiPack.lbl)

        self.varVae = tk.StringVar()
        self.cmbVae = ttk.Combobox(
            self.frmModelVae,
            textvariable=self.varVae,
            state="readonly",
            postcommand=self.listVaeValues,
            width=30,
            height=23,
        )
        self.cmbVae.pack(UiPack.cmb)
        self.listVaeValues()

        self.frmModelVae.pack(UiPack.frm)

    def listModeValues(self):
        baseQuery = os.path.join(Path.model, "**", "*.")
        models = glob.glob(baseQuery + "safetensors", recursive=True)
        models += glob.glob(baseQuery + "ckpt", recursive=True)
        pfxLen = len(Path.model) + 1
        models = [model[pfxLen:] for model in models]
        self.cmbModel.configure(values=models)

    def listVaeValues(self):
        baseQuery = os.path.join(Path.vae, "**", "*.")
        vaes = glob.glob(baseQuery + "safetensors", recursive=True)
        vaes += glob.glob(baseQuery + "pt", recursive=True)
        vaes += glob.glob(baseQuery + "ckpt", recursive=True)
        pfxLen = len(Path.vae) + 1
        vaes = [vae[pfxLen:] for vae in vaes]
        self.cmbVae.configure(values=vaes)

    def initSize(self):
        self.frmSize = ttk.Frame(self.frm)

        self.varWidth = tk.IntVar(value=384)
        self.varWidth.trace_add("write", self.updateSize)
        self.lblWidth = ttk.Label(self.frmSize, text=L10n.get("generate_width"))
        self.lblWidth.pack(UiPack.lbl)
        self.sliWidth = tk.Scale(
            self.frmSize,
            from_=256,
            to=768,
            resolution=BasicForm.sizeResolution,
            orient=tk.HORIZONTAL,
            variable=self.varWidth,
            takefocus=True,
        )
        self.sliWidth.pack(UiPack.sli)

        self.varHeight = tk.IntVar(value=512)
        self.varHeight.trace_add("write", self.updateSize)
        self.lblHeight = ttk.Label(self.frmSize, text=L10n.get("generate_height"))
        self.lblHeight.pack(UiPack.lbl)
        self.sliHeight = tk.Scale(
            self.frmSize,
            from_=256,
            to=768,
            resolution=BasicForm.sizeResolution,
            orient=tk.HORIZONTAL,
            variable=self.varHeight,
            takefocus=True,
        )

        self.sliHeight.pack(UiPack.sli)

        self.btnSwap = ttk.Button(
            self.frmSize, text=L10n.get("swap"), command=self.swapSize
        )
        self.btnSwap.pack(UiPack.btn)

        self.frmSize.pack(UiPack.frm)

    def swapSize(self):
        tmp = self.varWidth.get()
        self.varWidth.set(self.varHeight.get())
        self.varHeight.set(tmp)

    def initUpsdcale1(self):
        self.frmUpscale1 = ttk.Frame(self.frm)
        self.varUpscale1Enabled = tk.BooleanVar(value=True)
        self.chkUpscale1Enabled = tk.Checkbutton(
            self.frmUpscale1,
            text=L10n.get("upscale1"),
            variable=self.varUpscale1Enabled,
        )
        self.chkUpscale1Enabled.pack(UiPack.chk)

        self.varUpscale1Mode = tk.StringVar()
        self.cmbUpscale1Mode = ttk.Combobox(
            self.frmUpscale1,
            values=(
                Const.tile,
                Const.refine,
            ),
            textvariable=self.varUpscale1Mode,
            state="readonly",
            width=11,
        )
        self.cmbUpscale1Mode.current(1)
        self.cmbUpscale1Mode.pack(UiPack.cmb)

        self.lblUpscale1Scale = ttk.Label(
            self.frmUpscale1, text=L10n.get("upscale_scale")
        )
        self.lblUpscale1Scale.pack(UiPack.lbl)
        self.varUpscale1Scale = tk.DoubleVar(value=2.0)
        self.varUpscale1Scale.trace_add("write", self.updateSize)
        self.sliUpscale1Scale = tk.Scale(
            self.frmUpscale1,
            from_=1.0,
            to=4.0,
            resolution=0.5,
            orient=tk.HORIZONTAL,
            variable=self.varUpscale1Scale,
            takefocus=True,
        )
        self.sliUpscale1Scale.pack(UiPack.sli)

        self.frmUpscale1.pack(UiPack.frm)

    def initUpsdcale2(self):
        self.frmUpscale2 = ttk.Frame(self.frm)
        self.varUpscale2Enabled = tk.BooleanVar(value=True)
        self.chkUpscale2Enabled = tk.Checkbutton(
            self.frmUpscale2,
            text=L10n.get("upscale2"),
            variable=self.varUpscale2Enabled,
        )
        self.chkUpscale2Enabled.pack(UiPack.chk)

        self.varUpscale2Mode = tk.StringVar()
        self.cmbUpscale2Mode = ttk.Combobox(
            self.frmUpscale2,
            values=(
                Const.tile,
                Const.refine,
            ),
            textvariable=self.varUpscale2Mode,
            state="readonly",
            width=11,
        )
        self.cmbUpscale2Mode.current(0)
        self.cmbUpscale2Mode.pack(UiPack.cmb)

        self.lblUpscale2Scale = ttk.Label(
            self.frmUpscale2, text=L10n.get("upscale_scale")
        )
        self.lblUpscale2Scale.pack(UiPack.lbl)
        self.varUpscale2Scale = tk.DoubleVar(value=1.5)
        self.varUpscale2Scale.trace_add("write", self.updateSize)
        self.sliUpscale2Scale = tk.Scale(
            self.frmUpscale2,
            from_=1.0,
            to=4.0,
            resolution=0.5,
            orient=tk.HORIZONTAL,
            variable=self.varUpscale2Scale,
            takefocus=True,
        )
        self.sliUpscale2Scale.pack(UiPack.sli)

        self.frmUpscale2.pack(UiPack.frm)

    def updateSize(self, *args):
        width = self.varWidth.get()
        height = self.varHeight.get()
        upscale1 = self.varUpscale1Scale.get()
        upscale2 = upscale1 * self.varUpscale2Scale.get()

        self.chkUpscale1Enabled.configure(
            text=f"{L10n.get('upscale1')}\n{int(width * upscale1):>4} x {int(height * upscale1):>4}"
        )
        self.chkUpscale2Enabled.configure(
            text=f"{L10n.get('upscale2')}\n{int(width * upscale2):>4} x {int(height * upscale2):>4}"
        )

    @classmethod
    def loadConfig(self):
        BasicForm.maxLength = Config.get("ui", "max_length", BasicForm.maxLength)
        BasicForm.sizeResolution = Config.get(
            "ui", "size_resolution", BasicForm.sizeResolution
        )

    def storeConfig(self):
        Config.set("ui", "max_length", BasicForm.maxLength)
        Config.set("ui", "size_resolution", BasicForm.sizeResolution)

    def setDarkTheme(self, colors):
        self.sliLength.configure(colors["sli"])
        self.entSeed.configure(colors["ent"])
        self.sliWidth.configure(colors["sli"])
        self.sliHeight.configure(colors["sli"])
        self.chkUpscale1Enabled.configure(colors["chk"])
        self.sliUpscale1Scale.configure(colors["sli"])
        self.chkUpscale2Enabled.configure(colors["chk"])
        self.sliUpscale2Scale.configure(colors["sli"])

editor/src/ui_basic.py
Zuntan03/EasyPromptAnime
﻿import tkinter as tk


class Ui:
    padX = 4
    padY = 2
    cfgTxtArea = {"spacing1": 4, "spacing2": 4, "spacing3": 4, "wrap": tk.WORD}


class UiPack:
    txt = {"side": tk.LEFT, "fill": tk.BOTH, "expand": True}
    frm = {
        "padx": Ui.padX,
        "pady": Ui.padY,
        "fill": tk.X,
    }
    sli = {
        "padx": Ui.padX,
        "pady": Ui.padY,
        "side": tk.LEFT,
        "fill": tk.X,
        "expand": True,
    }
    sep = {"padx": Ui.padX, "pady": Ui.padY, "fill": tk.X}
    lbl = {"padx": Ui.padX, "pady": Ui.padY, "side": tk.LEFT}
    btn = {"padx": Ui.padX, "pady": Ui.padY, "side": tk.LEFT}
    chk = {"padx": Ui.padX, "pady": Ui.padY, "side": tk.LEFT}
    cmb = {"padx": Ui.padX, "pady": Ui.padY, "side": tk.LEFT}
    ent = {"padx": Ui.padX, "pady": Ui.padY, "side": tk.LEFT}

editor/src/ui_const.py
Zuntan03/EasyPromptAnime
﻿from const import Const, Path
from l10n import L10n
from config import Config
from ui_const import UiPack
import tkinter as tk
import tkinter.ttk as ttk
import os, glob


class GenerateForm:
    schedulerValues = Const.schedulerValues
    schedulerNames = Const.schedulerNames
    maxSteps = 60

    def __init__(self, parent):
        self.frm = ttk.Frame(parent)

        self.initMotionModuleContext()
        self.initSchedulerSteps()
        self.initGuidanceScaleClipScale()
        self.initPromptFixedRatioHalfVaeXFormers()

        parent.add(self.frm, text=L10n.get("generate_tab"))

    def initMotionModuleContext(self):
        self.frmMotionModuleContext = ttk.Frame(self.frm)

        self.lblMotionModule = ttk.Label(
            self.frmMotionModuleContext, text=L10n.get("generate_motion_module")
        )
        self.lblMotionModule.pack(UiPack.lbl)

        self.varMotionModule = tk.StringVar()
        self.cmbMotionModule = ttk.Combobox(
            self.frmMotionModuleContext,
            textvariable=self.varMotionModule,
            state="readonly",
            postcommand=self.listMotionModuleValues,
            width=40,
            height=23,
        )
        self.cmbMotionModule.pack(UiPack.cmb)
        self.listMotionModuleValues()

        self.lblContext = ttk.Label(
            self.frmMotionModuleContext, text=L10n.get("generate_context")
        )
        self.lblContext.pack(UiPack.lbl)
        self.varContext = tk.IntVar(value=14)
        self.sliContext = tk.Scale(
            self.frmMotionModuleContext,
            from_=8,
            to=24,
            resolution=2,
            orient=tk.HORIZONTAL,
            variable=self.varContext,
            takefocus=True,
        )
        self.sliContext.pack(UiPack.sli)

        self.frmMotionModuleContext.pack(UiPack.frm)

    def listMotionModuleValues(self):
        baseQuery = os.path.join(Path.motionModule, "**", "*.")
        motionModules = glob.glob(baseQuery + "ckpt", recursive=True)
        motionModules += glob.glob(baseQuery + "safetensors", recursive=True)
        motionModules += glob.glob(baseQuery + "pth", recursive=True)
        pfxLen = len(Path.motionModule) + 1
        motionModules = [motionModule[pfxLen:] for motionModule in motionModules]
        self.cmbMotionModule.configure(values=motionModules)

    def initSchedulerSteps(self):
        self.frmSchedulerSteps = ttk.Frame(self.frm)

        self.lblScheduler = ttk.Label(
            self.frmSchedulerSteps, text=L10n.get("generate_scheduler")
        )
        self.lblScheduler.pack(UiPack.lbl)

        self.varScheduler = tk.StringVar()
        self.cmbScheduler = ttk.Combobox(
            self.frmSchedulerSteps,
            textvariable=self.varScheduler,
            state="readonly",
            values=GenerateForm.schedulerNames,
            width=21,
            height=23,
        )
        self.cmbScheduler.pack(UiPack.cmb)

        self.lblSteps = ttk.Label(
            self.frmSchedulerSteps, text=L10n.get("generate_steps")
        )
        self.lblSteps.pack(UiPack.lbl)

        self.varSteps = tk.IntVar(value=20)
        self.sliSteps = tk.Scale(
            self.frmSchedulerSteps,
            from_=10,
            to=GenerateForm.maxSteps,
            resolution=1,
            orient=tk.HORIZONTAL,
            variable=self.varSteps,
            takefocus=True,
        )
        self.sliSteps.pack(UiPack.sli)

        self.frmSchedulerSteps.pack(UiPack.frm)

    def initGuidanceScaleClipScale(self):
        self.frmClipScaleGuidanceScale = ttk.Frame(self.frm)

        self.lblGuidanceScale = ttk.Label(
            self.frmClipScaleGuidanceScale, text=L10n.get("generate_guidance_scale")
        )
        self.lblGuidanceScale.pack(UiPack.lbl)

        self.varGuidanceScale = tk.DoubleVar(value=8.0)
        self.sliGuidanceScale = tk.Scale(
            self.frmClipScaleGuidanceScale,
            from_=1.0,
            to=30.0,
            resolution=0.5,
            orient=tk.HORIZONTAL,
            variable=self.varGuidanceScale,
            takefocus=True,
        )
        self.sliGuidanceScale.pack(UiPack.sli)

        self.lblClipSkip = ttk.Label(
            self.frmClipScaleGuidanceScale, text=L10n.get("generate_clip_skip")
        )
        self.lblClipSkip.pack(UiPack.lbl)

        self.varClipSkip = tk.IntVar(value=2)
        self.sliClipSkip = tk.Scale(
            self.frmClipScaleGuidanceScale,
            from_=1,
            to=5,
            resolution=1,
            orient=tk.HORIZONTAL,
            variable=self.varClipSkip,
            takefocus=True,
        )
        self.sliClipSkip.pack(UiPack.sli)

        self.frmClipScaleGuidanceScale.pack(UiPack.frm)

    def initPromptFixedRatioHalfVaeXFormers(self):
        self.frmPromptFixedRatioHalfVaeXFormers = ttk.Frame(self.frm)

        self.lblPromptFixedRatio = ttk.Label(
            self.frmPromptFixedRatioHalfVaeXFormers,
            text=L10n.get("generate_prompt_fixed_ratio"),
        )
        self.lblPromptFixedRatio.pack(UiPack.lbl)

        self.varPromptFixedRatio = tk.DoubleVar(value=0.5)
        self.sliPromptFixedRatio = tk.Scale(
            self.frmPromptFixedRatioHalfVaeXFormers,
            from_=0.0,
            to=1.0,
            resolution=0.1,
            orient=tk.HORIZONTAL,
            variable=self.varPromptFixedRatio,
            takefocus=True,
        )
        self.sliPromptFixedRatio.pack(UiPack.sli)

        self.varUseHalfVae = tk.BooleanVar(value=False)
        self.chkUseHalfVae = tk.Checkbutton(
            self.frmPromptFixedRatioHalfVaeXFormers,
            text=L10n.get("generate_use_half_vae"),
            variable=self.varUseHalfVae,
        )
        self.chkUseHalfVae.pack(UiPack.chk)

        self.varUseXFormers = tk.BooleanVar(value=False)
        self.chkUseXFormers = tk.Checkbutton(
            self.frmPromptFixedRatioHalfVaeXFormers,
            text=L10n.get("generate_use_x_formers"),
            variable=self.varUseXFormers,
        )
        self.chkUseXFormers.pack(UiPack.chk)

        self.frmPromptFixedRatioHalfVaeXFormers.pack(UiPack.frm)

    @classmethod
    def loadConfig(self):
        values = ",".join(GenerateForm.schedulerValues)
        values = Config.get("ui", "scheduler_values", values)
        GenerateForm.schedulerValues = [s.strip() for s in values.split(",")]

        names = ",".join(GenerateForm.schedulerNames)
        names = Config.get("ui", "scheduler_names", names)
        GenerateForm.schedulerNames = [s.strip() for s in names.split(",")]

        GenerateForm.maxSteps = Config.getInt("ui", "max_steps", GenerateForm.maxSteps)

    def storeConfig(self):
        Config.set("ui", "scheduler_values", ",".join(GenerateForm.schedulerValues))
        Config.set("ui", "scheduler_names", ",".join(GenerateForm.schedulerNames))
        Config.set("ui", "max_steps", GenerateForm.maxSteps)

    def setDarkTheme(self, colors):
        self.sliContext.configure(colors["sli"])
        self.sliSteps.configure(colors["sli"])
        self.sliClipSkip.configure(colors["sli"])
        self.sliGuidanceScale.configure(colors["sli"])
        self.sliPromptFixedRatio.configure(colors["sli"])
        self.chkUseHalfVae.configure(colors["chk"])
        self.chkUseXFormers.configure(colors["chk"])

editor/src/ui_generate.py
Zuntan03/EasyPromptAnime
﻿from config import Config
from l10n import L10n
import tkinter as tk
import tkinter.ttk as ttk
from tkinter import scrolledtext
from ui_const import Ui, UiPack
from task import Task


class InputForm:
    initH = 300
    minH = 250

    def __init__(self, parent):
        self.frm = ttk.Frame(parent)

        self.frmTool = ttk.Frame(self.frm)

        self.initGenerate(self.frmTool)
        self.sep0 = ttk.Separator(self.frmTool)
        self.sep0.pack(UiPack.sep)

        self.initPreview(self.frmTool)
        self.sep1 = ttk.Separator(self.frmTool)
        self.sep1.pack(UiPack.sep)

        self.initTask(self.frmTool)

        self.frmTool.pack(fill=tk.Y, side=tk.RIGHT)

        self.txtInput = scrolledtext.ScrolledText(self.frm, undo=True, maxundo=-1)
        self.txtInput.configure(Ui.cfgTxtArea)
        self.txtInput.pack(UiPack.txt)

        self.frm.pack(fill=tk.BOTH, expand=True)

        parent.add(
            self.frm, height=InputForm.initH, minsize=InputForm.minH, stretch="always"
        )

        self.initCtxMenu()

    def initGenerate(self, parent):
        self.frmGenerate = ttk.Frame(parent)
        self.btnGenerate = ttk.Button(self.frmGenerate, text=L10n.get("generate_anime"))
        self.btnGenerate.pack(UiPack.btn, fill=tk.X, expand=True)

        self.btnSeedGacha = ttk.Button(self.frmGenerate, text=L10n.get("seed_gacha"))
        self.btnSeedGacha.pack(UiPack.btn, fill=tk.X, expand=True)

        self.frmGenerate.pack(UiPack.frm)

    def initPreview(self, parent):
        self.frmPreview = ttk.Frame(parent)
        self.btnPreview = ttk.Button(self.frmPreview, text=L10n.get("preview"))
        self.btnPreview.pack(UiPack.btn)

        self.varUpscale = tk.BooleanVar(value=True)
        self.chkUpscale = tk.Checkbutton(
            self.frmPreview, text=L10n.get("preview_upscale"), variable=self.varUpscale
        )
        self.chkUpscale.pack(UiPack.chk)

        # self.varInterpolation = tk.BooleanVar(value=True)
        # self.chkInterpolation = tk.Checkbutton(
        #     self.frmPreview,
        #     text=L10n.get("preview_interpolation"),
        #     variable=self.varInterpolation,
        # )
        # self.chkInterpolation.pack(UiPack.chk)

        self.frmPreview.pack(UiPack.frm)

        self.frmStart = ttk.Frame(parent)
        self.varStart = tk.IntVar(value=0)
        self.lblStart = ttk.Label(self.frmStart, text=L10n.get("preview_start_frame"))
        self.lblStart.pack(UiPack.lbl)
        self.sliStart = tk.Scale(
            self.frmStart,
            from_=0,
            to=600,
            resolution=10,
            orient=tk.HORIZONTAL,
            variable=self.varStart,
            takefocus=True,
        )
        self.sliStart.pack(UiPack.sli)
        self.frmStart.pack(UiPack.frm)

        self.frmLength = ttk.Frame(parent)
        self.varLength = tk.IntVar(value=0)
        self.lblLength = ttk.Label(self.frmLength, text=L10n.get("preview_length"))
        self.lblLength.pack(UiPack.lbl)
        self.sliLength = tk.Scale(
            self.frmLength,
            from_=0,
            to=600,
            resolution=10,
            orient=tk.HORIZONTAL,
            variable=self.varLength,
            takefocus=True,
        )
        self.sliLength.pack(UiPack.sli)
        self.frmLength.pack(UiPack.frm)

    def initTask(self, parent):
        self.frmTask = ttk.Frame(parent)

        self.varForever = tk.BooleanVar(value=False)
        self.chkForever = tk.Checkbutton(
            self.frmTask, text=L10n.get("task_forever"), variable=self.varForever
        )
        self.chkForever.pack(UiPack.chk)

        self.varPauseByError = tk.BooleanVar(value=True)
        self.chkPauseByError = tk.Checkbutton(
            self.frmTask,
            text=L10n.get("task_pause_by_error"),
            variable=self.varPauseByError,
        )
        self.chkPauseByError.pack(UiPack.chk)

        self.frmTask.pack(UiPack.frm)

        self.varTask = tk.StringVar()
        Task.queueInfoChangedEvent.append(lambda queue: self.varTask.set(queue))
        self.lstTask = tk.Listbox(self.frmTool, listvariable=self.varTask)
        self.lstTask.pack(fill=tk.BOTH, expand=True, padx=Ui.padX, pady=Ui.padY)

    def initCtxMenu(self):
        self.mnuInTxt = tk.Menu(self.txtInput, tearoff=False)
        self.mnuInTxt.add_command(
            label=L10n.get("clear"),
            command=lambda: self.txtInput.delete("1.0", tk.END),
        )
        self.txtInput.bind(
            "<Button-3>", lambda e: self.mnuInTxt.post(e.x_root, e.y_root)
        )

    @classmethod
    def loadConfig(cls):
        InputForm.initH = Config.get("ui_size", "input_h", fallback=InputForm.initH)

    def storeConfig(self):
        input_h = self.frm.winfo_height()
        if input_h != 1:
            Config.set("ui_size", "input_h", input_h)

    def setDarkTheme(self, colors):
        self.txtInput.configure(colors["txt"])
        self.chkUpscale.configure(colors["chk"])
        # self.chkInterpolation.configure(colors["chk"])
        self.sliStart.configure(colors["sli"])
        self.sliLength.configure(colors["sli"])
        self.lstTask.configure(colors["lst"])
        self.chkForever.configure(colors["chk"])
        self.chkPauseByError.configure(colors["chk"])

editor/src/ui_input.py
Zuntan03/EasyPromptAnime
﻿import os, subprocess
import tkinter as tk
import tkinter.ttk as ttk
from const import Path
from l10n import L10n
from config import Config
from ui_const import UiPack


class IpAdapterForm:
    def __init__(self, parent):
        self.frm = ttk.Frame(parent)

        self.initFlags()
        self.initScale()
        self.initImageDir()
        self.initInfo()

        parent.add(self.frm, text=L10n.get("ip_adapter_tab"))

    def initFlags(self):
        self.frmFlags = ttk.Frame(self.frm)

        self.varUse = tk.BooleanVar()
        self.chkUse = tk.Checkbutton(
            self.frmFlags,
            text=L10n.get("ip_adapter_use"),
            variable=self.varUse,
        )
        self.chkUse.pack(UiPack.chk)

        self.varUsePlus = tk.BooleanVar()
        self.chkUsePlus = tk.Checkbutton(
            self.frmFlags,
            text=L10n.get("ip_adapter_use_plus"),
            variable=self.varUsePlus,
        )
        self.chkUsePlus.pack(UiPack.chk)

        self.varUsePlusFace = tk.BooleanVar()
        self.chkUsePlusFace = tk.Checkbutton(
            self.frmFlags,
            text=L10n.get("ip_adapter_use_plus_face"),
            variable=self.varUsePlusFace,
        )
        self.chkUsePlusFace.pack(UiPack.chk)

        self.frmFlags.pack(UiPack.frm)

    def initScale(self):
        self.frmScale = ttk.Frame(self.frm)

        self.lblScale = ttk.Label(self.frmScale, text=L10n.get("ip_adapter_scale"))
        self.lblScale.pack(UiPack.lbl)

        self.varScale = tk.DoubleVar(value=0.5)
        self.sliScale = tk.Scale(
            self.frmScale,
            from_=0.0,
            to=1.0,
            resolution=0.05,
            orient=tk.HORIZONTAL,
            variable=self.varScale,
            takefocus=True,
        )
        self.sliScale.pack(UiPack.sli)

        self.frmScale.pack(UiPack.frm)

    def initImageDir(self):
        self.frmImageDir = ttk.Frame(self.frm)

        self.lblImageDir = ttk.Label(
            self.frmImageDir, text=L10n.get("ip_adapter_image_dir")
        )
        self.lblImageDir.pack(UiPack.lbl)

        self.varImageDir = tk.StringVar()
        self.cmbImageDir = ttk.Combobox(
            self.frmImageDir,
            textvariable=self.varImageDir,
            state="readonly",
            postcommand=self.listImageDirValues,
            width=20,
            height=20,
        )
        self.cmbImageDir.pack(UiPack.cmb, fill=tk.X, expand=True)

        self.btnImageDirExplorer = ttk.Button(
            self.frmImageDir,
            text=L10n.get("ip_adapter_image_dir_explorer"),
            command=lambda: subprocess.run(["explorer", Path.ipAdapter]),
        )
        self.btnImageDirExplorer.pack(UiPack.btn)

        self.frmImageDir.pack(UiPack.frm)

    def listImageDirValues(self):
        subDirs = [f.name for f in os.scandir(Path.ipAdapter) if f.is_dir()]
        self.cmbImageDir.configure(values=subDirs)

    def initInfo(self):
        self.frmInfo = ttk.Frame(self.frm)

        self.lblIpAdapterInfo = ttk.Label(
            self.frmInfo, text=L10n.get("ip_adapter_info")
        )
        self.lblIpAdapterInfo.pack(UiPack.lbl)

        self.frmInfo.pack(UiPack.frm)

    def setDarkTheme(self, colors):
        self.chkUse.configure(colors["chk"])
        self.chkUsePlus.configure(colors["chk"])
        self.chkUsePlusFace.configure(colors["chk"])
        self.sliScale.configure(colors["sli"])

editor/src/ui_ip_adapter.py
Zuntan03/EasyPromptAnime
﻿import json
import tkinter as tk
import tkinter.ttk as ttk
from const import Path
from l10n import L10n
from ui_const import Ui, UiPack


class Menu:
    def __init__(self, parent):
        self.menuBar = tk.Menu(parent)
        parent.config(menu=self.menuBar)

        self.initFileMenu()
        self.initAnimeMenu()
        self.initFolderMenu()
        self.initToolMenu()
        self.initDownloadMenu()
        self.initHelpMenu()

    def initFileMenu(self):
        self.fileMenu = tk.Menu(self.menuBar, tearoff=False)
        self.menuBar.add_cascade(label=L10n.get("m_file"), menu=self.fileMenu)

        self.settingFileMenu = tk.Menu(self.fileMenu, tearoff=False)
        self.fileMenu.add_cascade(
            label=L10n.get("m_setting_file"), menu=self.settingFileMenu
        )

        self.settingFileMenu.add_command(label=L10n.get("m_open_ini_file"))
        self.settingFileMenu.add_command(label=L10n.get("m_open_default_prompt_file"))
        self.settingFileMenu.add_command(
            label=L10n.get("m_open_prompt_travel_template_file")
        )

    def initAnimeMenu(self):
        self.animeMenu = tk.Menu(self.menuBar, tearoff=False)
        self.menuBar.add_cascade(label=L10n.get("m_anime"), menu=self.animeMenu)

        self.animeMenu.add_command(label=L10n.get("m_anime_preview"))
        self.animeMenu.add_command(label=L10n.get("m_anime_seed_gacha"))
        self.animeMenu.add_command(label=L10n.get("m_anime_generate"))
        self.animeMenu.add_command(label=L10n.get("m_anime_upscale"))
        self.animeMenu.add_command(label=L10n.get("m_anime_upscale_config"))

    def initFolderMenu(self):
        self.folderMenu = tk.Menu(self.menuBar, tearoff=False)
        self.menuBar.add_cascade(label=L10n.get("m_folder"), menu=self.folderMenu)

        self.folderMenu.add_command(label=L10n.get("m_output_folder"))

        self.promptTravelFolderMenu = tk.Menu(self.folderMenu, tearoff=False)
        self.folderMenu.add_cascade(
            label=L10n.get("m_prompt_travel_folder"), menu=self.promptTravelFolderMenu
        )

        self.promptTravelFolderMenu.add_command(label=L10n.get("m_sd_model_folder"))
        self.promptTravelFolderMenu.add_command(
            label=L10n.get("m_motion_module_folder")
        )
        self.promptTravelFolderMenu.add_command(label=L10n.get("m_lora_folder"))
        self.promptTravelFolderMenu.add_command(label=L10n.get("m_embeddings_folder"))
        self.promptTravelFolderMenu.add_command(label=L10n.get("m_wildcard_folder"))

        self.promptTravelOutputFolderMenu = tk.Menu(self.folderMenu, tearoff=False)
        self.folderMenu.add_cascade(
            label=L10n.get("m_prompt_travel_output_folder"),
            menu=self.promptTravelOutputFolderMenu,
        )

        self.promptTravelOutputFolderMenu.add_command(
            label=L10n.get("m_prompt_travel_generate_folder")
        )
        self.promptTravelOutputFolderMenu.add_command(
            label=L10n.get("m_prompt_travel_tile_folder")
        )
        self.promptTravelOutputFolderMenu.add_command(
            label=L10n.get("m_prompt_travel_refine_folder")
        )

        self.folderMenu.add_command(label=L10n.get("m_temp_folder"))
        self.folderMenu.add_command(label=L10n.get("m_log_folder"))

    def initToolMenu(self):
        self.toolMenu = tk.Menu(self.menuBar, tearoff=False)
        self.menuBar.add_cascade(label=L10n.get("m_tool"), menu=self.toolMenu)

        self.toolMenu.add_command(label=L10n.get("m_tool_convert_lora"))

        self.toolMenu.add_command(label=L10n.get("m_tool_easy_leco"))

    def initDownloadMenu(self):
        self.downloadMenuData = ""
        with open(Path.downloadMenu, "r", encoding="utf-8-sig") as f:
            self.downloadMenuData = json.load(f)

        self.downloadMenu = tk.Menu(self.menuBar, tearoff=False)
        self.menuBar.add_cascade(label=L10n.get("m_download"), menu=self.downloadMenu)

        def initDlMenu(category):
            dlMenu = tk.Menu(self.downloadMenu, tearoff=False)
            self.downloadMenu.add_cascade(
                label=L10n.get(f"m_dl_{category}"), menu=dlMenu
            )

            for name, data in self.downloadMenuData[category].items():
                label = f'({data["tag"]}) {name}' if "tag" in data else name
                data["menu"] = dlMenu
                data["checked"] = tk.BooleanVar(value=True)
                dlMenu.add_checkbutton(label=label, variable=data["checked"])
            return dlMenu

        self.dlModel = initDlMenu("model")
        self.dlMotionModule = initDlMenu("motion_module")
        self.dlVae = initDlMenu("vae")
        self.dlEmbedding = initDlMenu("embedding")

    def initHelpMenu(self):
        self.helpMenu = tk.Menu(self.menuBar, tearoff=False)
        self.menuBar.add_cascade(label=L10n.get("m_help"), menu=self.helpMenu)

        self.helpMenu.add_command(label=L10n.get("m_prompt_help"))

        self.helpMenu.add_command(label=L10n.get("m_github"))

        self.referenceMenu = tk.Menu(self.helpMenu, tearoff=False)
        self.helpMenu.add_cascade(
            label=L10n.get("m_reference"), menu=self.referenceMenu
        )

        self.referenceMenu.add_command(label="AnimateDiff prompt travel")
        self.referenceMenu.add_command(label="sd-scripts")
        self.referenceMenu.add_command(label="Codex FFmpeg")
        self.referenceMenu.add_command(label="ECCV2022-RIFE")

editor/src/ui_menu.py
Zuntan03/EasyPromptAnime
﻿from config import Config
from l10n import L10n
import tkinter as tk
import tkinter.ttk as ttk
from tkinter import scrolledtext
from ui_const import Ui, UiPack


class OutputForm:
    initH = 100
    minH = 82

    def __init__(self, parent):
        self.ntb = ttk.Notebook(parent)
        parent.add(
            self.ntb, height=OutputForm.initH, minsize=OutputForm.minH, stretch="never"
        )

        self.txtUser = scrolledtext.ScrolledText(self.ntb, state=tk.DISABLED)
        self.txtUser.configure(Ui.cfgTxtArea)
        self.txtUser.pack(UiPack.txt)
        self.ntb.add(self.txtUser, text=L10n.get("user_log_tab"))

        self.txtSys = scrolledtext.ScrolledText(self.ntb, state=tk.DISABLED)
        self.txtSys.configure(Ui.cfgTxtArea)
        self.txtSys.pack(UiPack.txt)
        self.ntb.add(self.txtSys, text=L10n.get("system_log_tab"))

        # self.ntb.select(1)  # select system log tab

        self.initCtxMenu()

    def initCtxMenu(self):
        self.mnuUser = tk.Menu(self.txtUser, tearoff=False)
        self.mnuUser.add_command(
            label=L10n.get("clear"), command=lambda: self.txtUser.delete("1.0", tk.END)
        )

        self.txtUser.bind("<Button-3>", lambda e: self.mnuUser.post(e.x_root, e.y_root))

        self.mnuSys = tk.Menu(self.txtSys, tearoff=False)
        self.mnuSys.add_command(
            label=L10n.get("clear"), command=lambda: self.txtSys.delete("1.0", tk.END)
        )

        self.txtSys.bind("<Button-3>", lambda e: self.mnuSys.post(e.x_root, e.y_root))

    @classmethod
    def loadConfig(self):
        OutputForm.initH = Config.get("ui_size", "output_h", OutputForm.initH)

    def storeConfig(self):
        output_h = self.ntb.winfo_height()
        if output_h != 1:
            Config.set("ui_size", "output_h", output_h)

    def setDarkTheme(self, colors):
        self.txtUser.configure(colors["selIns"])
        self.txtSys.configure(colors["selIns"])

editor/src/ui_output.py
Zuntan03/EasyPromptAnime
﻿from config import Config
from l10n import L10n
import tkinter as tk
import tkinter.ttk as ttk
from tkinter import scrolledtext
from ui_const import Ui, UiPack


class PreviewForm:
    def __init__(self, parent):
        self.frm = ttk.Frame(parent)

        self.initOptions()

        self.txtPrev = scrolledtext.ScrolledText(self.frm, state=tk.DISABLED)
        self.txtPrev.configure(Ui.cfgTxtArea)
        self.txtPrev.pack(UiPack.txt)

        parent.add(self.frm, text=L10n.get("preview_tab"))

    def initOptions(self):
        self.frmOptions = ttk.Frame(self.frm)

        self.varShowKeyframe = tk.BooleanVar(value=True)
        self.chkShowKeyframe = tk.Checkbutton(
            self.frmOptions,
            text=L10n.get("preview_show_keyframe"),
            variable=self.varShowKeyframe,
        )
        self.chkShowKeyframe.pack(UiPack.chk)

        self.varShowHeaderFooter = tk.BooleanVar(value=True)
        self.chkShowHeaderFooter = tk.Checkbutton(
            self.frmOptions,
            text=L10n.get("preview_show_header_footer"),
            variable=self.varShowHeaderFooter,
        )
        self.chkShowHeaderFooter.pack(UiPack.chk)

        self.varShowKeyframe.trace_add("write", self.updateShowKeyframe)

        self.varShowAnime = tk.BooleanVar(value=True)
        self.chkShowAnime = tk.Checkbutton(
            self.frmOptions,
            text=L10n.get("preview_show_anime"),
            variable=self.varShowAnime,
        )
        self.chkShowAnime.pack(UiPack.chk)

        self.frmOptions.pack(UiPack.frm, side=tk.BOTTOM)

    def updateShowKeyframe(self, *args):
        if self.varShowKeyframe.get():
            self.chkShowHeaderFooter.configure(state=tk.NORMAL)
        else:
            self.chkShowHeaderFooter.configure(state=tk.DISABLED)

    def setDarkTheme(self, colors):
        self.chkShowKeyframe.configure(colors["chk"])
        self.chkShowHeaderFooter.configure(colors["chk"])
        self.chkShowAnime.configure(colors["chk"])
        self.txtPrev.configure(colors["txt"])

editor/src/ui_preview.py
Zuntan03/EasyPromptAnime
﻿from const import Const
from l10n import L10n
from config import Config
from ui_const import UiPack
import tkinter as tk
import tkinter.ttk as ttk


class UpscaleForm:
    schedulerValues = Const.schedulerValues
    schedulerNames = Const.schedulerNames
    maxSteps = 60

    def __init__(self, parent):
        self.frm = ttk.Frame(parent)

        self.initSchedulerSteps()
        self.initGuidanceScaleDenoise()
        self.initTile()
        self.initHalfVaeXFormers()

        parent.add(self.frm, text=L10n.get("upscale_tab"))

    def initSchedulerSteps(self):
        self.frmSchedulerSteps = ttk.Frame(self.frm)

        self.lblScheduler = ttk.Label(
            self.frmSchedulerSteps, text=L10n.get("upscale_scheduler")
        )
        self.lblScheduler.pack(UiPack.lbl)

        self.varScheduler = tk.StringVar()
        self.cmbScheduler = ttk.Combobox(
            self.frmSchedulerSteps,
            textvariable=self.varScheduler,
            state="readonly",
            values=UpscaleForm.schedulerNames,
            width=21,
            height=23,
        )
        self.cmbScheduler.pack(UiPack.cmb)

        self.lblSteps = ttk.Label(
            self.frmSchedulerSteps, text=L10n.get("upscale_steps")
        )
        self.lblSteps.pack(UiPack.lbl)
        self.varSteps = tk.IntVar(value=15)
        self.sliSteps = tk.Scale(
            self.frmSchedulerSteps,
            from_=10,
            to=UpscaleForm.maxSteps,
            resolution=1,
            orient=tk.HORIZONTAL,
            variable=self.varSteps,
            takefocus=True,
        )
        self.sliSteps.pack(UiPack.sli)

        self.frmSchedulerSteps.pack(UiPack.frm)

    def initGuidanceScaleDenoise(self):
        self.frmDenoiseGuidanceScale = ttk.Frame(self.frm)

        self.lblGuidanceScale = ttk.Label(
            self.frmDenoiseGuidanceScale, text=L10n.get("upscale_guidance_scale")
        )
        self.lblGuidanceScale.pack(UiPack.lbl)

        self.varGuidanceScale = tk.DoubleVar(value=8.0)
        self.sliGuidanceScale = tk.Scale(
            self.frmDenoiseGuidanceScale,
            from_=1.0,
            to=30.0,
            resolution=0.5,
            orient=tk.HORIZONTAL,
            variable=self.varGuidanceScale,
            takefocus=True,
        )
        self.sliGuidanceScale.pack(UiPack.sli)

        self.lblDenoise = ttk.Label(
            self.frmDenoiseGuidanceScale, text=L10n.get("upscale_strength")
        )
        self.lblDenoise.pack(UiPack.lbl)
        self.varDenoise = tk.DoubleVar(value=0.5)
        self.sliDenoise = tk.Scale(
            self.frmDenoiseGuidanceScale,
            from_=0.0,
            to=1.0,
            resolution=0.05,
            orient=tk.HORIZONTAL,
            variable=self.varDenoise,
            takefocus=True,
        )
        self.sliDenoise.pack(UiPack.sli)

        self.frmDenoiseGuidanceScale.pack(UiPack.frm)

    def initTile(self):
        self.frmTile = ttk.Frame(self.frm)

        self.lblTileScale = ttk.Label(self.frmTile, text=L10n.get("upscale_tile_scale"))
        self.lblTileScale.pack(UiPack.lbl)
        self.varTileScale = tk.DoubleVar(value=1.0)
        self.sliTileScale = tk.Scale(
            self.frmTile,
            from_=0.0,
            to=1.0,
            resolution=0.05,
            orient=tk.HORIZONTAL,
            variable=self.varTileScale,
            takefocus=True,
        )
        self.sliTileScale.pack(UiPack.sli)

        self.lblTileStart = ttk.Label(self.frmTile, text=L10n.get("upscale_tile_start"))
        self.lblTileStart.pack(UiPack.lbl)
        self.varTileStart = tk.DoubleVar(value=0.0)
        self.sliTileStart = tk.Scale(
            self.frmTile,
            from_=0.0,
            to=1.0,
            resolution=0.05,
            orient=tk.HORIZONTAL,
            variable=self.varTileStart,
            takefocus=True,
        )
        self.sliTileStart.pack(UiPack.sli)

        self.lblTileEnd = ttk.Label(self.frmTile, text=L10n.get("upscale_tile_end"))
        self.lblTileEnd.pack(UiPack.lbl)
        self.varTileEnd = tk.DoubleVar(value=1.0)
        self.sliTileEnd = tk.Scale(
            self.frmTile,
            from_=0.0,
            to=1.0,
            resolution=0.05,
            orient=tk.HORIZONTAL,
            variable=self.varTileEnd,
            takefocus=True,
        )
        self.sliTileEnd.pack(UiPack.sli)

        self.frmTile.pack(UiPack.frm)

    def initHalfVaeXFormers(self):
        self.frmHalfVaeXFormers = ttk.Frame(self.frm)

        self.varUseHalfVae = tk.BooleanVar(value=False)
        self.chkUseHalfVae = tk.Checkbutton(
            self.frmHalfVaeXFormers,
            text=L10n.get("upscale_use_half_vae"),
            variable=self.varUseHalfVae,
        )
        self.chkUseHalfVae.pack(UiPack.chk)

        self.varUseXFormers = tk.BooleanVar(value=False)
        self.chkUseXFormers = tk.Checkbutton(
            self.frmHalfVaeXFormers,
            text=L10n.get("upscale_use_x_formers"),
            variable=self.varUseXFormers,
        )
        self.chkUseXFormers.pack(UiPack.chk)

        self.frmHalfVaeXFormers.pack(UiPack.frm)

    @classmethod
    def loadConfig(self):
        values = ",".join(UpscaleForm.schedulerValues)
        values = Config.get("ui", "scheduler_values", values)
        UpscaleForm.schedulerValues = [s.strip() for s in values.split(",")]

        names = ",".join(UpscaleForm.schedulerNames)
        names = Config.get("ui", "scheduler_names", names)
        UpscaleForm.schedulerNames = [s.strip() for s in names.split(",")]

        UpscaleForm.maxSteps = Config.get("upscale", "max_steps", UpscaleForm.maxSteps)

    # def storeConfig(self): # by GenerateForm "scheduler_values", "scheduler_names", "max_steps"

    def setDarkTheme(self, colors):
        self.sliSteps.configure(colors["sli"])
        self.sliDenoise.configure(colors["sli"])
        self.sliGuidanceScale.configure(colors["sli"])
        self.sliTileScale.configure(colors["sli"])
        self.sliTileStart.configure(colors["sli"])
        self.sliTileEnd.configure(colors["sli"])
        self.chkUseHalfVae.configure(colors["chk"])
        self.chkUseXFormers.configure(colors["chk"])

editor/src/ui_upscale.py
Zuntan03/EasyPromptAnime