Spaces:
Runtime error
Runtime error
| import csv | |
| import os | |
| import secrets | |
| import sys | |
| from pathlib import Path | |
| from typing import Any | |
| from aiohttp import web | |
| from .log import mklog | |
| from .utils import ( | |
| backup_file, | |
| import_install, | |
| input_dir, | |
| output_dir, | |
| reqs_map, | |
| run_command, | |
| styles_dir, | |
| ) | |
| endlog = mklog("mtb endpoint") | |
| # - ACTIONS | |
| import_install("requirements") | |
| def ACTIONS_installDependency(dependency_names=None): | |
| if dependency_names is None: | |
| # return web.Response(text="No dependency name provided", status=400) | |
| return {"error": "No dependency name provided"} | |
| endlog.debug(f"Received Install Dependency request for {dependency_names}") | |
| # reqs = [] | |
| resolved_names = [reqs_map.get(name, name) for name in dependency_names] | |
| try: | |
| run_command( | |
| [Path(sys.executable), "-m", "pip", "install"] + resolved_names | |
| ) | |
| return {"success": True} | |
| except Exception as e: | |
| return {"error": f"Failed to install dependencies: {e}"} | |
| # if platform.system() == "Windows": | |
| # reqs = list(requirements.parse((here / "reqs_windows.txt").read_text())) | |
| # else: | |
| # reqs = list(requirements.parse((here / "reqs.txt").read_text())) | |
| # print([x.specs for x in reqs]) | |
| # print( | |
| # "\n".join([f"{x.line} {''.join(x.specs[0] if x.specs else '')}" for x in reqs]) | |
| # ) | |
| # for dependency_name in dependency_names: | |
| # for req in reqs: | |
| # if req.name == dependency_name: | |
| # endlog.debug(f"Dependency {dependency_name} installed") | |
| # break | |
| def ACTIONS_getUserImages( | |
| mode: str, | |
| count=200, | |
| offset=0, | |
| sort: str | None = None, | |
| include_subfolders: bool = False, | |
| ): | |
| # TODO: find a better name :s | |
| enabled = "MTB_EXPOSE" in os.environ | |
| if not enabled: | |
| return {"error": "Session not authorized to getInputs"} | |
| imgs = {} | |
| entry_dir = input_dir if mode == "input" else output_dir | |
| pattern = "**/*.png" if include_subfolders else "*.png" | |
| entry_gen = entry_dir.glob(pattern) | |
| entries = {} | |
| if sort: | |
| sort = sort.lower() | |
| if sort == "none": | |
| entries = entry_gen | |
| elif sort == "modified": | |
| entries = sorted( | |
| entry_gen, key=lambda x: x.stat().st_mtime, reverse=True | |
| ) | |
| elif sort == "modified-reverse": | |
| entries = sorted(entry_gen, key=lambda x: x.stat().st_mtime) | |
| elif sort == "name": | |
| entries = sorted(entry_gen, key=lambda x: x.name) | |
| elif sort == "name-reverse": | |
| entries = sorted(entry_gen, key=lambda x: x.name, reverse=True) | |
| else: | |
| endlog.warning(f"Sort mode {sort} not supported") | |
| entries = entry_gen | |
| else: | |
| entries = entry_gen | |
| for i, img in enumerate(entries): | |
| if i < offset: | |
| continue | |
| subfolder = ( | |
| img.parent.relative_to(entry_dir) if include_subfolders else "" | |
| ) | |
| imgs[img.stem] = ( | |
| f"/mtb/view?filename={img.name}&width=512&type={mode}&subfolder=" | |
| f"{subfolder}" | |
| f"&preview=&rand={secrets.randbelow(424242)}" | |
| ) | |
| if i >= count + offset - 1: | |
| break | |
| return imgs | |
| def ACTIONS_getStyles(style_name=None): | |
| from .nodes.conditions import MTB_StylesLoader | |
| styles = MTB_StylesLoader.options | |
| match_list = ["name"] | |
| if styles: | |
| filtered_styles = { | |
| key: value | |
| for key, value in styles.items() | |
| if not key.startswith("__") and key not in match_list | |
| } | |
| if style_name: | |
| return filtered_styles.get( | |
| style_name, {"error": "Style not found"} | |
| ) | |
| return filtered_styles | |
| return {"error": "No styles found"} | |
| def ACTIONS_saveStyle(data): | |
| # endlog.debug(f"Received Save Styles for {data.keys()}") | |
| # endlog.debug(data) | |
| styles = [f.name for f in styles_dir.iterdir() if f.suffix == ".csv"] | |
| target = None | |
| rows = [] | |
| for fp, content in data.items(): | |
| if fp in styles: | |
| endlog.debug(f"Overwriting {fp}") | |
| target = styles_dir / fp | |
| rows = content | |
| break | |
| if not target: | |
| endlog.warning( | |
| f"Could not determine the target file for {data.keys()}" | |
| ) | |
| return {"error": "Could not determine the target file for the style"} | |
| backup_file(target) | |
| with target.open("w", newline="", encoding="utf-8") as file: | |
| csv_writer = csv.writer(file, quoting=csv.QUOTE_ALL) | |
| for row in rows: | |
| csv_writer.writerow(row) | |
| async def do_action(request: web.Request) -> web.Response: | |
| endlog.debug("Init action request") | |
| request_data = await request.json() | |
| name = request_data.get("name") | |
| args = request_data.get("args") | |
| endlog.debug(f"Received action request: {name} {args}") | |
| method_name = f"ACTIONS_{name}" | |
| method = globals().get(method_name) | |
| if callable(method): | |
| result = None | |
| if args: | |
| result = method(*args) if isinstance(args, list) else method(args) | |
| else: | |
| result = method() | |
| endlog.debug(f"Action result: {result}") | |
| return web.json_response({"result": result}) | |
| available_methods = [ | |
| attr[len("ACTIONS_") :] | |
| for attr in globals() | |
| if attr.startswith("ACTIONS_") | |
| ] | |
| return web.json_response( | |
| { | |
| "error": "Invalid method name.", | |
| "available_methods": available_methods, | |
| } | |
| ) | |
| # - HTML UTILS | |
| def dependencies_button(name: str, dependencies: list[str]) -> str: | |
| deps = ",".join([f"'{x}'" for x in dependencies]) | |
| return f""" | |
| <button | |
| class="dependency-button" | |
| onclick="window.mtb_action('installDependency',[{deps}])" | |
| >Install {name} deps</button> | |
| """ | |
| def csv_editor(): | |
| inputs = [f for f in styles_dir.iterdir() if f.suffix == ".csv"] | |
| # rows = {f.stem: list(csv.reader(f.read_text("utf8"))) for f in styles} | |
| style_files = {} | |
| for file in inputs: | |
| with open(file, encoding="utf8") as f: | |
| parsed = csv.reader(f) | |
| style_files[file.name] = [] | |
| for row in parsed: | |
| endlog.debug(f"Adding style {row[0]}") | |
| style_files[file.name].append((row[0], row[1], row[2])) | |
| html_out = """ | |
| <div id="style-editor"> | |
| <h1>Style Editor</h1> | |
| """ | |
| for current, styles in style_files.items(): | |
| current_out = f"<h3>{current}</h3>" | |
| table_rows = [] | |
| for index, style in enumerate(styles): | |
| table_rows += ( | |
| (["<tr>"] + [f"<th>{cell}</th>" for cell in style] + ["</tr>"]) | |
| if index == 0 | |
| else ( | |
| ["<tr>"] | |
| + [ | |
| f"<td><input type='text' value='{cell}'></td>" | |
| if i == 0 | |
| else f"<td><textarea name='Text1' cols='40' rows='5'>{cell}</textarea></td>" | |
| for i, cell in enumerate(style) | |
| ] | |
| + ["</tr>"] | |
| ) | |
| ) | |
| current_out += ( | |
| f"<table data-id='{current}' data-filename='{current}'>" | |
| + "".join(table_rows) | |
| + "</table>" | |
| ) | |
| current_out += f"<button data-id='{current}' onclick='saveTableData(this.getAttribute(\"data-id\"))'>Save {current}</button>" | |
| html_out += add_foldable_region(current, current_out) | |
| html_out += "</div>" | |
| html_out += """<script src='/mtb-assets/js/saveTableData.js'></script>""" | |
| return html_out | |
| def render_tab_view(**kwargs): | |
| tab_headers = [] | |
| tab_contents = [] | |
| for idx, (tab_name, content) in enumerate(kwargs.items()): | |
| active_class = "active" if idx == 0 else "" | |
| tab_headers.append( | |
| f"<button class='tablinks {active_class}' onclick=\"openTab(event, '{tab_name}')\">{tab_name}</button>" | |
| ) | |
| tab_contents.append( | |
| f"<div id='{tab_name}' class='tabcontent {active_class}'>{content}</div>" | |
| ) | |
| headers_str = "\n".join(tab_headers) | |
| contents_str = "\n".join(tab_contents) | |
| return f""" | |
| <div class='tab-container'> | |
| <div class='tab'> | |
| {headers_str} | |
| </div> | |
| {contents_str} | |
| </div> | |
| <script src='/mtb-assets/js/tabSwitch.js'></script> | |
| """ | |
| def add_foldable_region(title: str, content: str): | |
| symbol_id = f"{title}-symbol" | |
| return f""" | |
| <div class='foldable'> | |
| <div | |
| class='foldable-title' | |
| onclick="toggleFoldable('{title}', '{symbol_id}')" | |
| > | |
| <span id='{symbol_id}' class='foldable-symbol'>▷</span> | |
| {title} | |
| </div> | |
| <div id='{title}' class='foldable-content'> | |
| {content} | |
| </div> | |
| </div> | |
| <script src='/mtb-assets/js/foldable.js'></script> | |
| """ | |
| def add_split_pane( | |
| left_content: str, right_content: str, *, vertical: bool = True | |
| ): | |
| orientation = "vertical" if vertical else "horizontal" | |
| return f""" | |
| <div class="split-pane {orientation}"> | |
| <div id="leftPane"> | |
| {left_content} | |
| </div> | |
| <div id="resizer"></div> | |
| <div id="rightPane"> | |
| {right_content} | |
| </div> | |
| </div> | |
| <script> | |
| initSplitPane({str(vertical).lower()}); | |
| </script> | |
| <script src='/mtb-assets/js/splitPane.js'></script> | |
| """ | |
| def add_dropdown(title: str, options: list[str]): | |
| option_str = "\n".join( | |
| [f"<option value='{opt}'>{opt}</option>" for opt in options] | |
| ) | |
| return f""" | |
| <select> | |
| <option disabled selected>{title}</option> | |
| {option_str} | |
| </select> | |
| """ | |
| def render_table(table_dict: dict[str, Any], sort=True, title=None): | |
| table_list = sorted( | |
| table_dict.items(), key=lambda item: item[0] | |
| ) # Sort the dictionary by keys | |
| table_rows = "" | |
| for name, item in table_list: | |
| if isinstance(item, dict): | |
| if "dependencies" in item: | |
| table_rows += f"<tr><td>{name}</td><td>" | |
| table_rows += ( | |
| f"{dependencies_button(name,item['dependencies'])}" | |
| ) | |
| table_rows += "</td></tr>" | |
| else: | |
| table_rows += ( | |
| f"<tr><td>{name}</td><td>{render_table(item)}</td></tr>" | |
| ) | |
| # elif isinstance(item, str): | |
| # table_rows += f"<tr><td>{name}</td><td>{item}</td></tr>" | |
| else: | |
| table_rows += f"<tr><td>{name}</td><td>{item}</td></tr>" | |
| return f""" | |
| <div class="table-container"> | |
| {"" if title is None else f"<h1>{title}</h1>"} | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Name</th> | |
| <th>Description</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {table_rows} | |
| </tbody> | |
| </table> | |
| </div> | |
| """ | |
| def render_base_template(title: str, content: str): | |
| github_icon_svg = """<svg xmlns="http://www.w3.org/2000/svg" fill="whitesmoke" height="3em" viewBox="0 0 496 512"><path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"/></svg>""" | |
| return f""" | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>{title}</title> | |
| <link rel="stylesheet" href="/mtb-assets/style.css"/> | |
| </head> | |
| <script type="module"> | |
| import {{ api }} from '/scripts/api.js' | |
| const mtb_action = async (action, args) =>{{ | |
| console.log(`Sending ${{action}} with args: ${{args}}`) | |
| }} | |
| window.mtb_action = async (action, args) =>{{ | |
| console.log(`Sending ${{action}} with args: ${{args}} to the API`) | |
| const res = await api.fetchApi('/actions', {{ | |
| method: 'POST', | |
| body: JSON.stringify({{ | |
| name: action, | |
| args, | |
| }}), | |
| }}) | |
| const output = await res.json() | |
| console.debug(`Received ${{action}} response:`, output) | |
| if (output?.result?.error){{ | |
| alert(`An error occured: {{output?.result?.error}}`) | |
| }} | |
| return output?.result | |
| }} | |
| </script> | |
| <body> | |
| <header> | |
| <a href="/">Back to Comfy</a> | |
| <div class="mtb_logo"> | |
| <img | |
| src="https://repository-images.githubusercontent.com/649047066/a3eef9a7-20dd-4ef9-b839-884502d4e873" | |
| alt="Comfy MTB Logo" height="70" width="128"> | |
| <span class="title">Comfy MTB</span></div> | |
| <a style="width:128px;text-align:center" href="https://www.github.com/melmass/comfy_mtb"> | |
| {github_icon_svg} | |
| </a> | |
| </header> | |
| <main> | |
| {content} | |
| </main> | |
| <footer> | |
| <!-- Shared footer content here --> | |
| </footer> | |
| </body> | |
| </html> | |
| """ | |