├── Discord.png ├── images ├── DeepMake.png ├── Discord.png └── Youtube_thumbnail.jpg ├── run.bat ├── gui.css ├── .gitignore ├── Create_Conda_envs.bat ├── environment.yml ├── test_log_file.json ├── startup.py ├── gui_info.json ├── LICENSE ├── mac_show_ui.py ├── db_utils.py ├── routers ├── login.py ├── ui.py ├── report.py └── plugin_manager.py ├── storage_db.py ├── readme.md ├── finalize_install.py ├── update_gui.py ├── auth_handler.py ├── plugin ├── datatypes.py └── __init__.py ├── install_mac.sh ├── pyqt_gui_table.py ├── main.py └── gui.py /Discord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMakeStudio/DeepMake/HEAD/Discord.png -------------------------------------------------------------------------------- /images/DeepMake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMakeStudio/DeepMake/HEAD/images/DeepMake.png -------------------------------------------------------------------------------- /images/Discord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMakeStudio/DeepMake/HEAD/images/Discord.png -------------------------------------------------------------------------------- /images/Youtube_thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMakeStudio/DeepMake/HEAD/images/Youtube_thumbnail.jpg -------------------------------------------------------------------------------- /run.bat: -------------------------------------------------------------------------------- 1 | %echo off 2 | CALL "%HomePath%\Miniconda3\Scripts\activate.bat" 3 | CALL conda activate deepmake 4 | cd %~dp0 5 | python startup.py -------------------------------------------------------------------------------- /gui.css: -------------------------------------------------------------------------------- 1 | QPushButton:pressed {{ 2 | background-color : #7b3bff; 3 | }} 4 | 5 | QPushButton {{ 6 | color: {QTMATERIAL_SECONDARYCOLOR}; 7 | text-transform: none; 8 | background-color: #7b3bff; 9 | border: #7b3bff; 10 | }} 11 | 12 | .big_button {{ 13 | height: 64px; 14 | }} 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.dll 8 | *.so 9 | 10 | #redis dbs 11 | *.rdb 12 | 13 | #debug folders 14 | debug/ 15 | 16 | #build folder 17 | build/ 18 | bin/ 19 | 20 | #huey 21 | huey.db* 22 | huey_storage.db* 23 | 24 | #plugins 25 | plugin/ 26 | -------------------------------------------------------------------------------- /Create_Conda_envs.bat: -------------------------------------------------------------------------------- 1 | %echo off 2 | CALL "%HomePath%\Miniconda3\Scripts\activate.bat" 3 | 4 | copy "%~dp0\environment.yml" "%temp%\DeepMake_environment.yml" 5 | copy "%~dp0\plugin\Diffusers\environment.yml" "%temp%\Diffusers_environment.yml 6 | 7 | %echo on 8 | CALL conda env update -f "%temp%\DeepMake_environment.yml" 9 | CALL conda env update -f "%temp%\Diffusers_environment.yml" 10 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: deepmake 2 | channels: 3 | - defaults 4 | dependencies: 5 | - pip 6 | - python=3.10 7 | - pip: 8 | - anyio 9 | - click 10 | - fastapi 11 | - gradio 12 | - h11 13 | - huey 14 | - idna 15 | - pydantic 16 | - pymemcache 17 | - python-multipart 18 | - requests 19 | - sentry-sdk[fastapi] 20 | - sniffio 21 | - starlette 22 | - uvicorn 23 | - psutil 24 | - pyjwt 25 | - pyside6 26 | -------------------------------------------------------------------------------- /test_log_file.json: -------------------------------------------------------------------------------- 1 | { 2 | "logs": [ 3 | { 4 | "timestamp": "2024-04-01T12:00:00", 5 | "level": "INFO", 6 | "message": "Application started successfully." 7 | }, 8 | { 9 | "timestamp": "2024-04-01T12:10:00", 10 | "level": "WARNING", 11 | "message": "An unexpected condition was encountered." 12 | }, 13 | { 14 | "timestamp": "2024-04-01T12:15:00", 15 | "level": "ERROR", 16 | "message": "Failed to connect to the database." 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /startup.py: -------------------------------------------------------------------------------- 1 | # import uvicorn 2 | # from main import app 3 | import subprocess 4 | import sys 5 | import requests 6 | import time 7 | 8 | client = requests.Session() 9 | 10 | if __name__ == "__main__": 11 | if sys.platform != "win32": 12 | main_proc = subprocess.Popen(f"uvicorn main:app --host 127.0.0.1 --port 8000 --log-level info".split()) 13 | else: 14 | main_proc = subprocess.Popen(f"uvicorn main:app --host 127.0.0.1 --port 8000 --log-level info", shell=True) 15 | 16 | pid = main_proc.pid 17 | time.sleep(3) 18 | r = client.get(f"http://127.0.0.1:8000/get_main_pid/{pid}") 19 | # uvicorn.run("main:app", host="127.0.0.1", port=8000, log_level="info") -------------------------------------------------------------------------------- /gui_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin":{ 3 | "Diffusers":{ 4 | "url":"https://github.com/DeepMakeStudio/Diffusers", 5 | "repo":"GitHub - DeepMakeStudio/Diffusers: Generate images using Diffusers", 6 | "Version": "0.1.0", 7 | "Description":"A diffusers based plugin implementing Stable Diffusion image generation.", 8 | "size": "1 GB" 9 | }, 10 | "Bisenet": { 11 | "url": "https://github.com/DeepMakeStudio/Bisenet", 12 | "repo": "GitHub - DeepMakeStudio/Bisenet: Bisenet implementation for DeepMakeStudio", 13 | "Version": "0.1.0", 14 | "Description": "A Bisenet based plugin implementing image segmentation.", 15 | "size": "500 MB" 16 | }, 17 | "Dummy": { 18 | "url": "https://github.com/DeepMakeStudio/Dummy", 19 | "repo": "Testing", 20 | "Version": "0.1.0", 21 | "Description": "Dummy", 22 | "size": "500 MB" 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 DeepMake 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /mac_show_ui.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QApplication 2 | import sys 3 | from gui import ConfigGUI, PluginManagerGUI, Updater, ReportIssueDialog, LoginWidget 4 | import argparse 5 | from PySide6.QtGui import QScreen 6 | 7 | parser = argparse.ArgumentParser() 8 | 9 | parser.add_argument("-n", "--ui_name", help="Database name") 10 | parser.add_argument("-p", "--plugin_name", help="Database name") 11 | 12 | 13 | args = parser.parse_args() 14 | 15 | 16 | def center_screen(screen): 17 | center = QScreen.availableGeometry(QApplication.primaryScreen()).center() 18 | geo = screen.frameGeometry() 19 | geo.moveCenter(center) 20 | screen.move(geo.topLeft()) 21 | 22 | ui_name = args.ui_name 23 | plugin_name = args.plugin_name 24 | app = QApplication(sys.argv) 25 | 26 | if ui_name == "PluginManager": 27 | window = PluginManagerGUI() 28 | elif ui_name == "Config": 29 | window = ConfigGUI(plugin_name) 30 | elif ui_name == "Updater": 31 | window = Updater() 32 | elif ui_name == "ReportIssueDialog": 33 | window = ReportIssueDialog() 34 | elif ui_name == "Login": 35 | window = LoginWidget() 36 | 37 | window.show() 38 | center_screen(window) 39 | 40 | try: 41 | app.exec() 42 | sys.exit(0) 43 | except: 44 | pass 45 | 46 | 47 | -------------------------------------------------------------------------------- /db_utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import sqlite3 4 | import json 5 | from fastapi import HTTPException 6 | 7 | if sys.platform == "win32": 8 | storage_folder = os.path.join(os.getenv('APPDATA'),"DeepMake") 9 | elif sys.platform == "darwin": 10 | storage_folder = os.path.join(os.getenv('HOME'),"Library","Application Support","DeepMake") 11 | elif sys.platform == "linux": 12 | storage_folder = os.path.join(os.getenv('HOME'),".local", "DeepMake") 13 | 14 | def store_data(key: str, item: dict): 15 | conn = sqlite3.connect(os.path.join(storage_folder, 'data_storage.db')) 16 | cursor = conn.cursor() 17 | value = json.dumps(dict(item)) 18 | cursor.execute("REPLACE INTO key_value_store (key, value) VALUES (?, ?)", (key, value)) 19 | conn.commit() 20 | conn.close() 21 | return {"message": "Data stored successfully"} 22 | 23 | def retrieve_data(key: str): 24 | conn = sqlite3.connect(os.path.join(storage_folder, 'data_storage.db')) 25 | cursor = conn.cursor() 26 | cursor.execute("SELECT value FROM key_value_store WHERE key = ?", (key,)) 27 | row = cursor.fetchone() 28 | conn.close() 29 | if row: 30 | data = json.loads(row[0]) 31 | return data 32 | raise HTTPException(status_code=404, detail="Key not found") 33 | 34 | def delete_data(key: str): 35 | conn = sqlite3.connect(os.path.join(storage_folder, 'data_storage.db')) 36 | cursor = conn.cursor() 37 | cursor.execute("DELETE FROM key_value_store WHERE key = ?", (key,)) 38 | conn.commit() 39 | conn.close() 40 | return {"message": "Data deleted successfully"} 41 | -------------------------------------------------------------------------------- /routers/login.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, FastAPI, Depends, Request, Header 2 | import requests 3 | from auth_handler import auth_handler as auth 4 | from pydantic import BaseModel 5 | 6 | class LoginRequest(BaseModel): 7 | username: str 8 | password: str 9 | 10 | router = APIRouter() 11 | client = requests.Session() 12 | 13 | # @router.get("/") 14 | # def some_router_function(request: Request): 15 | # global auth 16 | # auth = request.app.state.resource 17 | # return {"auth": auth} 18 | 19 | @router.get("/status") 20 | async def get_login_status(): 21 | print(auth.logged_in) 22 | return {"logged_in": auth.logged_in} 23 | 24 | @router.post("/login") 25 | async def login(request: LoginRequest): 26 | if auth.login_with_credentials(request.username, request.password): 27 | return {"status": "success", "message": "Logged in successfully"} 28 | else: 29 | return {"status": "failed", "message": "Login failed"} 30 | 31 | @router.get("/logout") 32 | async def logout(): 33 | 34 | auth.logout() 35 | return {"status": "success", "message": "Logged out successfully"} 36 | 37 | @router.get("/username") 38 | async def get_username(): 39 | return {"username": auth.username} 40 | 41 | @router.get("/get_url") 42 | async def get_file(url: str): 43 | 44 | return auth.get_url(url) 45 | 46 | @router.get("/check_login") 47 | async def check_login(): 48 | if auth.logged_in: 49 | user = auth.get_user_info() 50 | return {'logged_in': True, 'email': user['email'], 'roles': auth.roles} 51 | else: 52 | return {'logged_in': False} 53 | 54 | @router.get("/subscription_level") 55 | async def subscription_level(): 56 | return {"status": "success", "subscription_level": auth.permission_level()} -------------------------------------------------------------------------------- /storage_db.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import sqlite3 3 | import json 4 | import os 5 | 6 | if sys.platform == "win32": 7 | storage_folder = os.path.join(os.getenv('APPDATA'),"DeepMake") 8 | elif sys.platform == "darwin": 9 | storage_folder = os.path.join(os.getenv('HOME'),"Library","Application Support","DeepMake") 10 | elif sys.platform == "linux": 11 | storage_folder = os.path.join(os.getenv('HOME'),".local", "DeepMake") 12 | 13 | if not os.path.exists(storage_folder): 14 | os.mkdir(storage_folder) 15 | 16 | class storage_db: 17 | def __init__(self): 18 | self.storage_db = os.path.join(storage_folder, 'data_storage.db') 19 | self.init_db() 20 | 21 | def init_db(self): 22 | conn = sqlite3.connect(self.storage_db) 23 | cursor = conn.cursor() 24 | cursor.execute(""" 25 | CREATE TABLE IF NOT EXISTS key_value_store ( 26 | key TEXT PRIMARY KEY, 27 | value TEXT 28 | ) 29 | """) 30 | conn.commit() 31 | conn.close() 32 | 33 | def store_data(self, key: str, item: dict): 34 | try: 35 | conn = sqlite3.connect(self.storage_db) 36 | cursor = conn.cursor() 37 | value = json.dumps(dict(item)) 38 | cursor.execute("REPLACE INTO key_value_store (key, value) VALUES (?, ?)", (key, value)) 39 | conn.commit() 40 | conn.close() 41 | return True 42 | except: 43 | return False 44 | 45 | def retrieve_data(self, key: str): 46 | conn = sqlite3.connect(self.storage_db) 47 | cursor = conn.cursor() 48 | cursor.execute("SELECT value FROM key_value_store WHERE key = ?", (key,)) 49 | row = cursor.fetchone() 50 | conn.close() 51 | if row: 52 | data = json.loads(row[0]) 53 | return data 54 | return False 55 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # DeepMake 2 | 3 | DeepMake uses generative AI make content creation fast and easy. We leverage the leading open source AI to give you VFX in a few clicks, create stock video from text prompts, instantly segment layers, and more. 4 | 5 | ![Deepmake Logo](images/DeepMake.png) 6 | 7 | # Support 8 | 9 | For support please [![Join our Discord server](images/Discord.png)](https://discord.gg/E6T5t7mE8T) or visit [our support page](https://deepmake.com/support). 10 | 11 | # DeepMake Backend 12 | 13 | This repo contains the backend for DeepMake software. It requires host plugins (Such as our After Effects plugin) as well as processing plugins (such as our Diffusers plugin for Text to Image generation) 14 | 15 | # Installation 16 | 17 | ## Install the Deepmake Backend 18 | ### Easy install 19 | 20 | #### MacOS (Apple Silicon only) 21 | For Mac OS we have an easy installer available. Simply run 22 | 23 | `curl -s -L https://raw.githubusercontent.com/DeepMakeStudio/DeepMake/main/install_mac.sh -o install_mac.sh && sudo sh ./install_mac.sh` 24 | 25 | It should handle all the installation steps for the backend and you can continue to [Install any processing plugins you want](#Install any processing plugins you want) 26 | 27 | #### Windows 28 | 29 | For windows, Download the installer and run it to install DeepMake. 30 | 31 | [Download the Installer](https://github.com/DeepMakeStudio/DeepMake/releases/latest/download/DeepMake_Win_Installer.exe) 32 | 33 | ### Manual Install 34 | If you used the easy installer, you don't need to manually install, but if you do not want to install using the installer, you can install manually. 35 | 36 | For the most up-to-date instructions for manual installation, please see the [installation guide on our website](https://deepmake.com/install/#manual-installation) 37 | 38 | # Usage 39 | 40 | Now that you've completed installation you're ready to use DeepMake 41 | 42 | ## After Effects 43 | 44 | To use DeepMake simply activate the plugin from Effects/DeepMake/AI Plugin Renderer 45 | 46 | For a video guide: 47 | 48 | [![Loading DeepMake in After Effects Video Guide](https://img.youtube.com/vi/wQzkNe4Bh3c/0.jpg)](https://www.youtube.com/watch?v=wQzkNe4Bh3c) 49 | 50 | Then you may choose from the installed plugins. Each processing plugin will have its own settings for you to configure. DeepMake automatically makes the options that each processning plugin use visibile for you to modify. 51 | 52 | ## For more 53 | 54 | For more information or if you want to get more help see [DeepMake.com](https://deepmake.com/) or join our [Discord server](https://discord.gg/E6T5t7mE8T) 55 | 56 | New Guides, Videos, and tutorials will be released over time. 57 | -------------------------------------------------------------------------------- /routers/ui.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from PySide6.QtWidgets import QApplication 3 | from PySide6.QtGui import QScreen 4 | import sys 5 | import os 6 | import subprocess 7 | import requests 8 | 9 | router = APIRouter() 10 | 11 | @router.get("/plugin_manager") 12 | def plugin_manager(): 13 | # global app 14 | # if sys.platform != "darwin": 15 | # if app is None: 16 | # app = QApplication(sys.argv) 17 | # window = PluginManagerGUI() 18 | # window.show() 19 | # center_screen(window) 20 | 21 | # try: 22 | # sys.exit(app.exec()) 23 | # except: 24 | # pass 25 | # else: 26 | subprocess.Popen("python mac_show_ui.py -n PluginManager".split()) 27 | 28 | @router.get("/configure/{plugin_name}") 29 | def plugin_config_ui(plugin_name: str): 30 | 31 | try: 32 | r = requests.get(f"http://127.0.0.1:8000/plugins/get_config/{plugin_name}") 33 | print(r.status_code) 34 | print(r.json()) 35 | except: 36 | return {"status": "error", "message": "Please start the plugin first."} 37 | 38 | # if sys.platform != "darwin": 39 | # if app is None: 40 | # app = QApplication(sys.argv) 41 | # window = ConfigGUI(plugin_name) 42 | # window.show() 43 | # center_screen(window) 44 | 45 | # try: 46 | # sys.exit(app.exec()) 47 | # except: 48 | # pass 49 | # else: 50 | subprocess.Popen(f"python mac_show_ui.py -n Config -p {plugin_name}".split()) 51 | 52 | @router.get("/updater") 53 | def update_gui(): 54 | # global app 55 | 56 | # if sys.platform != "darwin": 57 | # if app is None: 58 | # app = QApplication(sys.argv) 59 | # window = Updater() 60 | # window.show() 61 | # center_screen(window) 62 | 63 | # try: 64 | # sys.exit(app.exec()) 65 | # except: 66 | # pass 67 | # else: 68 | subprocess.Popen("python mac_show_ui.py -n Updater".split()) 69 | 70 | 71 | 72 | @router.get("/report_issue") 73 | def report_issue(): 74 | # global app 75 | 76 | # if sys.platform != "darwin": 77 | # #log_file_path = '/home/andresca94/DeepMake/test_log_file.json' 78 | # if app is None: 79 | # app = QApplication(sys.argv) 80 | # #window = ReportIssueDialog(logFilePath=log_file_path) 81 | # window = ReportIssueDialog() 82 | # window.show() 83 | # app.exec() 84 | 85 | # try: 86 | # sys.exit(app.exec()) 87 | # except: 88 | # pass 89 | # else: 90 | subprocess.Popen("python mac_show_ui.py -n ReportIssueDialog".split()) 91 | 92 | @router.get("/login") 93 | def login(): 94 | # global app 95 | 96 | # if sys.platform != "darwin": 97 | # if app is None: 98 | # app = QApplication(sys.argv) 99 | # window = LoginWidget() 100 | # window.show() 101 | # app.exec() 102 | 103 | # try: 104 | # sys.exit(app.exec()) 105 | # except: 106 | # pass 107 | # else: 108 | subprocess.Popen("python mac_show_ui.py -n Login".split()) -------------------------------------------------------------------------------- /routers/report.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import sys 4 | import subprocess 5 | from fastapi import APIRouter, Form, HTTPException 6 | import requests 7 | import io 8 | import zipfile 9 | import sentry_sdk 10 | from sentry_sdk import capture_message, configure_scope 11 | #from sentry_sdk.integrations.huey import HueyIntegration 12 | 13 | router = APIRouter() 14 | 15 | @router.post("/report/") 16 | async def report_issue(description: str = Form(...), log_file_path: str = Form(None)): 17 | print("Received report issue request.") 18 | print(f"Description: {description}") 19 | 20 | try: 21 | with configure_scope() as scope: 22 | if log_file_path and os.path.isfile(log_file_path): 23 | with open(log_file_path, 'r') as log_file: 24 | log_data = json.load(log_file) 25 | log_contents = json.dumps(log_data, indent=4).encode() # Encode the pretty JSON to bytes 26 | scope.add_attachment(bytes=log_contents, filename=os.path.basename(log_file_path), content_type="application/json") 27 | print("Log file attached successfully.") 28 | else: 29 | print("No log file was attached or log file does not exist.") 30 | 31 | capture_message("Issue report created by user\n" + str(description)) 32 | except Exception as e: 33 | print(f"Error processing the report: {e}") 34 | raise HTTPException(status_code=500, detail=f"Error processing the report: {e}") 35 | 36 | return {"message": "Issue reported successfully"} 37 | 38 | """ 39 | @router.post("/report/") 40 | async def report_issue(description: str = Form(...), log_file_path: str = Form(None)): 41 | print("Received report issue request.") 42 | print(f"Description: {description}") 43 | 44 | try: 45 | with configure_scope() as scope: 46 | # Attach the description as a text snippet 47 | scope.add_attachment(bytes=description.encode(), filename="description.txt", content_type="text/plain") 48 | 49 | if log_file_path and os.path.isfile(log_file_path): 50 | # Attach the log file directly if it exists 51 | scope.add_attachment(path=log_file_path, content_type="text/plain") 52 | print("Log file attached successfully.") 53 | else: 54 | print("No log file was attached or log file does not exist.") 55 | 56 | # Capture the message once all attachments are added 57 | capture_message("Issue report processed") 58 | except Exception as e: 59 | print(f"Error processing the report: {e}") 60 | raise HTTPException(status_code=500, detail=f"Error processing the report: {e}") 61 | 62 | return {"message": "Issue reported successfully"} 63 | """ 64 | 65 | """ 66 | @router.post("/report/") 67 | async def report_issue(description: str = Form(...), log_file_path: str = Form(None)): 68 | print("Received report issue request.") 69 | print(f"Description: {description}") 70 | #capture_message(description) # Log the basic description first 71 | #print("Sentry captured the basic description.") 72 | 73 | if log_file_path and os.path.isfile(log_file_path): 74 | print(f"Log file path provided: {log_file_path}") 75 | try: 76 | with open(log_file_path, 'r') as file: 77 | log_contents = file.read() 78 | print("Log file read successfully. Attaching contents to Sentry.") 79 | # Use a scope to attach additional data to Sentry events 80 | with configure_scope() as scope: 81 | scope.set_extra("log_contents", log_contents) 82 | capture_message("Log file attached") # This message will include the log_contents extra data 83 | print("Sentry captured the message with log contents.") 84 | except Exception as e: 85 | print(f"Error reading log file: {log_file_path}. Exception: {e}") 86 | # Log the exception with a scope, if needed 87 | with configure_scope() as scope: 88 | scope.set_extra("log_file_path", log_file_path) 89 | capture_message(f"Error reading log file: {e}") 90 | raise HTTPException(status_code=500, detail=f"Error reading log file: {e}") 91 | else: 92 | print("No log file was attached or log file does not exist.") 93 | capture_message("No log file was attached") 94 | 95 | return {"message": "Issue reported successfully"} 96 | """ -------------------------------------------------------------------------------- /finalize_install.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import sys 3 | import requests 4 | import time 5 | import uuid 6 | from hashlib import md5 7 | from datetime import datetime 8 | import json 9 | import os 10 | 11 | client = requests.Session() 12 | def get_id(): # return md5 hash of uuid.getnode() 13 | return md5(str(uuid.getnode()).encode()).hexdigest() 14 | 15 | def get_id(): # return md5 hash of uuid.getnode() 16 | return md5(str(uuid.getnode()).encode()).hexdigest() 17 | 18 | def send_sentry(message): 19 | # Your Sentry DSN 20 | DSN = "https://d4853d3e3873643fa675bc620a58772c@o4506430643175424.ingest.sentry.io/4506463076614144" 21 | 22 | # Extract the Sentry host and project ID from the DSN 23 | dsn_parsed = DSN.split('/') 24 | project_id = "4506463076614144" 25 | project_key = "d4853d3e3873643fa675bc620a58772c" 26 | sentry_host = dsn_parsed[2] 27 | 28 | # Generate a unique event ID 29 | event_id = uuid.uuid4().hex 30 | 31 | # Current timestamp 32 | timestamp = datetime.utcnow().isoformat() 33 | 34 | # Construct the event payload 35 | event_payload = { 36 | "event_id": event_id, 37 | "timestamp": timestamp, 38 | "level": "debug", 39 | "message": message, 40 | "user": { 41 | "id": get_id() 42 | }, 43 | "tags": { 44 | "os": sys.platform 45 | } 46 | } 47 | envelope_header = { 48 | "event_id": event_id, 49 | "dsn": DSN 50 | } 51 | envelope_data = { 52 | "type": "event", 53 | "content_type": "application/json", 54 | "length": len(json.dumps(event_payload)), 55 | "filename": "application.log" 56 | } 57 | 58 | # Construct the envelope 59 | envelope = f"{json.dumps(envelope_header)}\n{json.dumps(envelope_data)}\n{json.dumps(event_payload)}".strip() 60 | 61 | # Sentry Envelope endpoint 62 | url = f"https://{sentry_host}/api/{project_id}/envelope/" 63 | 64 | # Construct the authentication header 65 | auth_header = f"Sentry sentry_key={project_key}, sentry_version=7" 66 | 67 | # Send the envelope 68 | response = requests.post(url, data=envelope, headers={"Content-Type": "application/x-sentry-envelope", "X-Sentry-Auth": auth_header}) 69 | 70 | return response 71 | 72 | if __name__ == "__main__": 73 | if sys.platform == "win32": 74 | config_path = os.path.join(os.path.expandvars("%appdata%"),"DeepMake/Config.json") 75 | elif sys.platform == "darwin": 76 | config_path = os.path.join(os.path.expanduser("~/Library/Application Support/DeepMake/Config.json")) 77 | else: 78 | send_sentry(f"Failed to start backend\nOS is invalid\n{sys.platform}") 79 | config_path = os.path.join(os.path.expanduser("~/.config/DeepMake/Config.json")) 80 | if not os.path.exists(config_path): 81 | send_sentry(f"Failed to start backend\nConfig file not found") 82 | raise Exception(f"Config file not found: {config_path}") 83 | config_data = json.load(open(config_path,'r')) 84 | try: 85 | command = (config_data['Py_Environment'] + config_data['Startup_CMD']).replace("conda activate", "conda run -n").replace(";","").strip() 86 | directory = config_data['Directory'].replace("cd ","").replace("\\","").replace(";","").strip() 87 | except Exception as e: 88 | send_sentry(f"Failed to start backend\nConfig file missing required fields\n{e}") 89 | raise e 90 | 91 | try: 92 | if sys.platform != "win32": 93 | main_proc = subprocess.Popen(command.split(), cwd=directory) 94 | else: 95 | main_proc = subprocess.Popen(command, cwd=directory) 96 | pid = main_proc.pid 97 | except Exception as e: 98 | send_sentry(f"Failed to start backend\n{e}\nCommand: {command}") 99 | raise e 100 | 101 | delayed = 0 102 | r = None 103 | while delayed < 30: 104 | try: 105 | time.sleep(1) 106 | delayed += 1 107 | r = client.get(f"http://127.0.0.1:8000/get_main_pid/{pid}", timeout=1) 108 | except Exception as e: 109 | pass 110 | if r is None: 111 | send_sentry(f"Failed to start backend\nTimeout after {delayed} seconds") 112 | raise Exception("nTimeout after {delayed} seconds") 113 | 114 | try: 115 | client.get("http://127.0.0.1:8000/backend/shutdown") 116 | except: 117 | pass 118 | send_sentry("Backend test successful") -------------------------------------------------------------------------------- /update_gui.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QApplication, QWidget, QPushButton, QProgressBar, QComboBox, QHBoxLayout, QListWidget, QHeaderView, QTableWidget, QVBoxLayout, QTableWidgetItem, QDialog, QScrollArea, QDialogButtonBox 2 | import subprocess 3 | import sys 4 | 5 | 6 | class Updater(QWidget): 7 | def __init__(self): 8 | super().__init__() 9 | # self.name = plugin_name 10 | self.setWindowTitle("Update") 11 | self.setGeometry(0, 0, 500, 200) 12 | 13 | 14 | # self.button = QPushButton("Uninstall") 15 | # self.buttonBox.addButton("Uninstall", QDialogButtonBox.ButtonRole.AcceptRole) 16 | # self.buttonBox.addButton("Update to Latest", QDialogButtonBox.ButtonRole.RejectRole) 17 | # self.buttonBox.accepted.connect(self.accept) 18 | # self.buttonBox.centerButtons() 19 | # uninstall_button = QPushButton() 20 | # uninstall_button.addButton("Uninstall", QDialogButtonBox.ButtonRole.AcceptRole) 21 | self.layout = QVBoxLayout() 22 | self.createTable() 23 | self.update_button = QPushButton("Update to Latest") 24 | self.update_button.clicked.connect(self.update_plugin) 25 | # self.test_button = QPushButton("Test") 26 | # self.tableWidget.setCellWidget(1,0, self.buttonBox) 27 | # self.tableWidget.setCellWidget(1, 2, self.update_button) 28 | # self.tableWidget.setCellWidget(1, 1,self.test_button) 29 | # self.test_button.clicked.connect(self.test_plugin) 30 | # message = QLabel("Do you want to uninstall?") 31 | # self.layout.addWidget(message) 32 | self.layout.addWidget(self.tableWidget) 33 | self.layout.addWidget(self.update_button) 34 | # self.layout.addWidget(self.buttonBox) 35 | self.setLayout(self.layout) 36 | 37 | def test_plugin(self): 38 | self.tableWidget.setItem(0, 1, QTableWidgetItem("Testing...")) 39 | 40 | 41 | def update_plugin(self): 42 | if len(self.tag_list) == 0: 43 | print("No versioning") 44 | return 45 | version = self.update_button.text().split()[-1] 46 | if version == "Latest": 47 | version = self.tag_list[0] 48 | print("Updating to version", version) 49 | 50 | p = subprocess.Popen(f"git checkout {version} ".split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE) 51 | p.wait() 52 | # print(p.communicate()) 53 | # current_tag = self.getVersion() 54 | self.tableWidget.setItem(0, 0, QTableWidgetItem(f"Current Version: {version}")) 55 | print("Updated to version", version) 56 | 57 | def version_management(self): 58 | tag_list = [] 59 | tags = subprocess.check_output("git tag".split()).decode("utf-8") 60 | for label in tags.split("\n")[:-1]: 61 | tag_list.append(label) 62 | print(tag_list) 63 | return tag_list 64 | 65 | def createTable(self): 66 | self.tableWidget = QTableWidget() 67 | row_count = 2 68 | column_count = 3 69 | 70 | self.tableWidget.setRowCount(row_count) 71 | self.tableWidget.setColumnCount(column_count) 72 | self.button_dict = {} 73 | current_tag = self.getVersion() 74 | self.tableWidget.setItem(0, 0, QTableWidgetItem(f"Current Version: {current_tag}")) 75 | self.tableWidget.setItem(0, 1, QTableWidgetItem("Available Versions")) 76 | dropdown = QComboBox() 77 | self.tag_list = self.version_management() 78 | self.tag_list.reverse() 79 | dropdown.addItems(self.tag_list) 80 | dropdown.currentTextChanged.connect(self.changeVersion) 81 | 82 | self.tableWidget.setCellWidget(0, 2, dropdown) 83 | 84 | self.tableWidget.setShowGrid(False) 85 | self.tableWidget.horizontalHeader().setVisible(False) 86 | self.tableWidget.verticalHeader().setVisible(False) 87 | # self.tableWidget.setHorizontalHeaderLabels(['Name', 'Description', 'Version', 'Install']) 88 | 89 | self.tableWidget.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) 90 | self.tableWidget.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) 91 | self.tableWidget.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) 92 | def changeVersion(self, text): 93 | self.update_button.setText(f"Update to {text}") 94 | 95 | def getVersion(self): 96 | try: 97 | tag = subprocess.check_output("git describe --tags".split()).decode("utf-8").split("\n")[0] 98 | except: 99 | tag = "0.0.0" 100 | print(tag) 101 | # print(tag) 102 | return tag -------------------------------------------------------------------------------- /routers/plugin_manager.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import subprocess 4 | from fastapi import BackgroundTasks, APIRouter 5 | import requests 6 | import io 7 | from auth_handler import auth_handler as auth 8 | import zipfile 9 | from db_utils import retrieve_data 10 | from plugin import Plugin 11 | from argparse import Namespace 12 | import time 13 | 14 | router = APIRouter() 15 | client = requests.Session() 16 | 17 | def handle_install(plugin_name: str): 18 | plugin_dict = plugin_info() 19 | url = plugin_dict[plugin_name]["url"] 20 | cur_folder = os.getcwd() 21 | folder_path = os.path.join("plugin", plugin_name) 22 | plugin_folder_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "plugin") 23 | if ".git" in url: 24 | if sys.platform != "win32": 25 | p = subprocess.Popen(f"git clone {url} {folder_path}".split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE) 26 | else: 27 | p = subprocess.Popen(f"git clone {url} {folder_path}", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 28 | (out, err) = p.communicate() 29 | print(out, err) 30 | if "already exists" in err.decode("utf-8"): 31 | print("Plugin already installed") 32 | else: 33 | print("Installed", plugin_name) 34 | else: 35 | installed = False 36 | while not installed: 37 | r = auth.get_url(url) 38 | try: 39 | z = zipfile.ZipFile(io.BytesIO(r)) 40 | z.extractall(plugin_folder_path) 41 | installed = True 42 | except zipfile.BadZipFile: 43 | print("Bad Zip File") 44 | if sys.platform != "win32": 45 | p = subprocess.Popen(f"git submodule update --init".split(), cwd=folder_path, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 46 | else: 47 | p = subprocess.Popen(f"git submodule update --init", cwd=folder_path, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 48 | os.chdir(cur_folder) 49 | 50 | if sys.platform != "win32": 51 | if sys.platform == "darwin": 52 | popen_string = f"conda env create -f {folder_path}/environment_mac.yml" 53 | else: 54 | popen_string = f"conda env create -f {folder_path}/environment.yml" 55 | p = subprocess.Popen(popen_string.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE) 56 | else: 57 | p = subprocess.Popen(f"conda env create -f {folder_path}/environment.yml", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 58 | p.wait() 59 | r = client.get(f"http://127.0.0.1:8000/plugins/reload") 60 | 61 | #Start plugin and wait for it to start 62 | r = client.get("http://127.0.0.1:8000/plugins/get_states") 63 | if r.status_code == 200: 64 | result = r.json() 65 | if plugin_name not in result: 66 | return {"status": "failure", "message": "Plugin not installed"} 67 | r = client.get(f"http://127.0.0.1:8000/plugins/start_plugin/{plugin_name}") 68 | while result[plugin_name]["state"] != "RUNNING": 69 | time.sleep(10) 70 | r = client.get("http://127.0.0.1:8000/plugins/get_states") 71 | if r.status_code == 200: 72 | result = r.json() 73 | r = client.get(f"http://127.0.0.1:8000/plugins/stop_plugin/{plugin_name}") 74 | 75 | return {"status": "success"} 76 | 77 | 78 | @router.get("/install/{plugin_name}") 79 | async def install_plugin(plugin_name: str, background_tasks: BackgroundTasks): 80 | background_tasks.add_task(handle_install, plugin_name) 81 | 82 | return {"status": "installing"} 83 | 84 | @router.get("/uninstall/{plugin_name}") 85 | async def uninstall_plugin(plugin_name: str): 86 | print("CURRENT:", os.getcwd()) 87 | 88 | folder_path = os.path.join("plugin", plugin_name) 89 | print(folder_path) 90 | if sys.platform != "win32": 91 | p = subprocess.Popen(f"rm -rf {folder_path}".split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE) 92 | else: 93 | p = subprocess.Popen(f"rmdir /s /q {folder_path}", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 94 | # args = r.json() 95 | # args["plugin_name"] = plugin_name 96 | # dummy_plugin = Plugin(Namespace(**args)) 97 | # dummy_plugin._on_uninstall(args.config["model_urls"]) 98 | # Plugin().on_uninstall() 99 | return {"status": "success"} 100 | 101 | @router.get("/update/{plugin_name}/{version}") 102 | def update_plugin(plugin_name: str, version: str): 103 | if plugin_name != "DeepMake": 104 | plugin_dict = plugin_info() 105 | 106 | plugin_url = plugin_dict[plugin_name]["url"] 107 | folder_path = os.path.join("plugin", plugin_name) 108 | 109 | if plugin_name == "DeepMake" or ".git" in plugin_url: 110 | origin_folder = os.path.dirname(os.path.dirname(__file__)) 111 | if plugin_name != "DeepMake": 112 | os.chdir(os.path.join(origin_folder, "plugin", plugin_name)) 113 | # print(p.communicate()) 114 | print(f"git checkout {version}", os.getcwd()) 115 | if sys.platform != "win32": 116 | p = subprocess.Popen(f"git checkout {version} ".split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE) 117 | else: 118 | p = subprocess.Popen(f"git checkout {version} ", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 119 | p.wait() 120 | os.chdir(origin_folder) 121 | else: 122 | zip_name = plugin_url.split("/")[-1].split("-")[0] 123 | new_version_url = "/".join(plugin_url.split("/")[:-1]) + "/" + f"{zip_name}-{version}.zip" 124 | print(new_version_url) 125 | r = auth.get_url(new_version_url) 126 | try: 127 | z = zipfile.ZipFile(io.BytesIO(r)) 128 | z.extractall(folder_path) 129 | except zipfile.BadZipFile: 130 | print("Bad Zip File") 131 | return {"status": "failure", "message": "Bad Zip File"} 132 | 133 | return {"status": "success", "version": f"{version}"} 134 | 135 | @router.get("/get_plugin_info") 136 | def plugin_info(): 137 | print(auth.logged_in) 138 | try: 139 | plugin_dict = auth.get_url("https://deepmake.com/plugins.json") 140 | except: 141 | print("Error retrieving plugin info, using cached version") 142 | plugin_dict = retrieve_data("plugin_info") 143 | return plugin_dict 144 | 145 | -------------------------------------------------------------------------------- /auth_handler.py: -------------------------------------------------------------------------------- 1 | # Website login class, handles authentication of users, refreshing the jwt key as needed, and requesting user information 2 | import jwt 3 | import requests 4 | import time 5 | from storage_db import storage_db 6 | 7 | class AuthHandler(): 8 | def __init__(self, refresh_token = None, JWT = None, username = None, password = None): 9 | self.JWT = None 10 | self.refresh_token = None 11 | self.storage = storage_db() 12 | 13 | if JWT is not None: 14 | self.JWT = JWT 15 | if username is not None and password is not None: 16 | self.login_with_credentials(username, password) 17 | elif refresh_token is not None: 18 | self.refresh_token = refresh_token 19 | elif self.refresh_token is None: 20 | auth_token = self.storage.retrieve_data('auth') 21 | if auth_token: 22 | self.refresh_token = auth_token['refresh_token'] 23 | if JWT in auth_token: 24 | self.JWT = auth_token['JWT'] 25 | self.validate_jwt() 26 | else: 27 | self.refresh_login() 28 | 29 | 30 | @property 31 | def decoded_jwt(self): 32 | return jwt.decode(self.JWT, options={"verify_signature": False}) 33 | 34 | @property 35 | def logged_in(self): 36 | if self.JWT is not None: 37 | return self.validate_jwt() 38 | else: 39 | return False 40 | 41 | @property 42 | def username(self): 43 | if self.logged_in: 44 | return self.decoded_jwt['email'] 45 | else: 46 | return None 47 | 48 | @property 49 | def roles(self): 50 | return self.check_roles() 51 | 52 | def update_jwt(self, jwt_token): 53 | self.JWT = jwt_token 54 | 55 | def login_with_credentials(self, username, password): 56 | credentials = {'username': username, 'password': password, 'grant_type': 'password'} 57 | login_response = requests.post('https://deepmake.com/.netlify/identity/token',data=credentials) 58 | if login_response.status_code == 200: 59 | self.refresh_token = login_response.json()['refresh_token'] 60 | self.update_jwt(login_response.json()['access_token']) 61 | self.storage.store_data('auth', {'refresh_token': self.refresh_token, 'JWT': login_response.json()['access_token']}) 62 | return True 63 | else: 64 | return False 65 | 66 | def refresh_login(self): 67 | if self.refresh_token is None: 68 | return False 69 | refresh_response = requests.post('https://deepmake.com/.netlify/identity/token',data={'refresh_token': self.refresh_token, 'grant_type': 'refresh_token'}) 70 | if refresh_response.status_code == 200: 71 | self.refresh_token = refresh_response.json()['refresh_token'] 72 | self.update_jwt(refresh_response.json()['access_token']) 73 | self.storage.store_data('auth', {'refresh_token': self.refresh_token, 'JWT': refresh_response.json()['access_token']}) 74 | return True 75 | else: 76 | return False 77 | 78 | def get_user_info(self): 79 | user_info = requests.get('https://deepmake.com/.netlify/identity/user', headers={'Authorization': 'Bearer ' + self.JWT}) 80 | if user_info.status_code == 200: 81 | return user_info.json() 82 | else: 83 | return False 84 | 85 | def validate_jwt(self): 86 | if self.JWT is None: 87 | return False 88 | if self.decoded_jwt['exp'] < time.time(): 89 | return self.refresh_login() 90 | else: 91 | return True 92 | 93 | def get_url(self, url): 94 | self.validate_jwt() 95 | headers = {'Authorization': f'Bearer {self.JWT}', 'Cookie': f'nf_jwt={self.JWT}'} 96 | response = None 97 | if url.endswith('.zip'): 98 | return self.large_download(url) 99 | else: 100 | response = requests.get(url, headers=headers) 101 | 102 | if response.status_code == 200: 103 | if response.headers['Content-Type'] == 'application/json': 104 | return response.json() 105 | elif response.headers['Content-Type'] == 'application/zip': 106 | return response.content 107 | elif response.headers['Content-Type'] == 'text/html': 108 | return response.text 109 | else: 110 | return False 111 | 112 | def large_download(self, url): 113 | start = 0 114 | data = bytearray() 115 | headers = {'Authorization': f'Bearer {self.JWT}', 'Cookie': f'nf_jwt={self.JWT}'} 116 | 117 | while True: 118 | try: 119 | # Set the range header to request a subset of the file 120 | headers['Range'] = f'bytes={start}-' 121 | response = requests.get(url, headers=headers, stream=True) 122 | response.raise_for_status() # Check for request errors 123 | if response.status_code > 299: 124 | return response 125 | 126 | # Read the content in chunks 127 | for chunk in response.iter_content(chunk_size=1024): # 1KB chunks 128 | if chunk: 129 | data.extend(chunk) 130 | start += len(chunk) 131 | 132 | # If the loop completes without errors, break out of the while loop 133 | break 134 | 135 | except requests.exceptions.ChunkedEncodingError: 136 | # print("Chunked Encoding Error occurred, retrying...") 137 | continue # Continue the loop and try to reconnect from where it left off 138 | 139 | except Exception as e: 140 | # print(f"An error occurred: {e}") 141 | break 142 | 143 | return data 144 | 145 | 146 | 147 | def check_roles(self): 148 | if not self.logged_in: 149 | return [] 150 | if not self.validate_jwt(): 151 | return [] 152 | user_info = self.get_user_info() 153 | if 'roles' not in user_info['app_metadata']: 154 | return [] 155 | return user_info['app_metadata']['roles'] 156 | 157 | def check_permissions(self, level=1): 158 | if not self.logged_in: 159 | return False 160 | if level == 0: 161 | return True 162 | roles = self.check_roles() 163 | if len(roles) > 0: 164 | return True 165 | else: 166 | return False 167 | 168 | def permission_level(self): 169 | if not self.logged_in: 170 | return False 171 | roles = self.check_roles() 172 | if len(roles) > 0: 173 | return 1 174 | else: 175 | return 0 176 | 177 | def logout(self): 178 | self.JWT = None 179 | self.refresh_token = None 180 | self.storage.store_data('auth', {}) 181 | return True 182 | 183 | auth_handler = AuthHandler() -------------------------------------------------------------------------------- /plugin/datatypes.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | class Text(): 4 | default = None 5 | max_length = None 6 | help = None 7 | optional = None 8 | 9 | def __init__(self, default=None, max_length=None, help=None, optional=None): 10 | self.default = default 11 | self.max_length = max_length 12 | self.help = help 13 | self.optional = optional 14 | 15 | @property 16 | def data(self): 17 | return {"default": self.default, "help": self.help, "max_length": self.max_length, "optional": self.optional} 18 | 19 | @property 20 | def __str__(self): 21 | return "Text" + json.dumps({k: v for k, v in self.data.items() if v is not None}).replace('{','(').replace('}',')').replace(': ','=').replace('"','') 22 | 23 | @property 24 | def __dict__(self): 25 | return {k: v for k, v in self.data.items() if v is not None} 26 | 27 | class Image(): 28 | Height = None 29 | Width = None 30 | channels = "RGB" 31 | dtype = "float32" 32 | help = None 33 | optional = None 34 | 35 | def __init__(self, Height=None, Width=None, channels="RGB", dtype="float32", help=None, optional=None): 36 | self.Height = Height 37 | self.Width = Width 38 | self.channels = channels 39 | self.dtype = dtype 40 | self.help = help 41 | self.optional = optional 42 | 43 | @property 44 | def data(self): 45 | return {"Height": self.Height, "Width": self.Width, "channels": self.channels, "dtype": self.dtype, "help": self.help, "optional": self.optional} 46 | 47 | def __str__(self): 48 | return "Image" + json.dumps({k: v for k, v in self.data.items() if v is not None}).replace('{','(').replace('}',')').replace(': ','=').replace('"','') 49 | 50 | def __dict__(self): 51 | return {k: v for k, v in self.data.items() if v is not None} 52 | 53 | class Boolean(): 54 | default = None 55 | help = None 56 | optional = None 57 | 58 | def __init__(self, default=None, help=None, optional=None): 59 | self.default = default 60 | self.help = help 61 | self.optional = optional 62 | 63 | @property 64 | def data(self): 65 | return {"default": self.default, "help": self.help, "optional": self.optional} 66 | 67 | @property 68 | def __str__(self): 69 | return "Bool" + json.dumps({k: v for k, v in self.data.items() if v is not None}).replace('{','(').replace('}',')').replace(': ','=').replace('"','') 70 | 71 | @property 72 | def __dict__(self): 73 | return {k: v for k, v in self.data.items() if v is not None} 74 | 75 | class Integer(): 76 | default = None 77 | min = None 78 | max = None 79 | help = None 80 | optional = None 81 | 82 | def __init__(self, default=None, min=None, max=None, help=None, optional=None): 83 | self.default = default 84 | self.min = min 85 | self.max = max 86 | self.help = help 87 | self.optional = optional 88 | 89 | @property 90 | def data(self): 91 | return {"default": self.default, "min": self.min, "max": self.max, "help": self.help, "optional": self.optional} 92 | 93 | @property 94 | def __str__(self): 95 | return "Int" + json.dumps({k: v for k, v in self.data.items() if v is not None}).replace('{','(').replace('}',')').replace(': ','=').replace('"','') 96 | 97 | @property 98 | def __dict__(self): 99 | return {k: v for k, v in self.data.items() if v is not None} 100 | 101 | class Point(): 102 | x = None 103 | y = None 104 | relative = None 105 | help = None 106 | optional = None 107 | 108 | def __init__(self, x=None, y=None, relative=Boolean(default=False, help="Whether the point is relative to the image size or pixelwise"), help=None, optional=None): 109 | self.x = x 110 | self.y = y 111 | self.relative = relative 112 | self.help = help 113 | self.optional = optional 114 | 115 | @property 116 | def data(self): 117 | return {"x": self.x, "y": self.y, "relative": self.relative, "help": self.help, "optional": self.optional} 118 | 119 | @property 120 | def __str__(self): 121 | return "Point" + json.dumps({k: v for k, v in self.data.items() if v is not None}).replace('{','(').replace('}',')').replace(': ','=').replace('"','') 122 | 123 | @property 124 | def __dict__(self): 125 | return {k: v for k, v in self.data.items() if v is not None} 126 | 127 | class Box(): 128 | top_left = None 129 | bottom_right = None 130 | relative = None 131 | help = None 132 | optional = None 133 | 134 | def __init__(self, top_left=None, bottom_right=None, relative=Boolean(default=False, help="Whether the box is relative to the image size or pixelwise"), help=None, optional=None): 135 | self.top_left = top_left 136 | self.bottom_right = bottom_right 137 | self.relative = relative 138 | self.help = help 139 | self.optional = optional 140 | 141 | @property 142 | def data(self): 143 | return {"top_left": self.top_left, "bottom_right": self.bottom_right, "relative": self.relative, "help": self.help, "optional": self.optional} 144 | 145 | @property 146 | def __str__(self): 147 | return "Box" + json.dumps({k: v for k, v in self.data.items() if v is not None}).replace('{','(').replace('}',')').replace(': ','=').replace('"','') 148 | 149 | @property 150 | def __dict__(self): 151 | return {k: v for k, v in self.data.items() if v is not None} 152 | 153 | class Float(): 154 | default = None 155 | min = None 156 | max = None 157 | dtype = "float32" 158 | help = None 159 | optional = None 160 | 161 | def __init__(self, default=None, min=None, max=None, dtype="float32", help=None, optional=None): 162 | self.default = default 163 | self.min = min 164 | self.max = max 165 | self.dtype = dtype 166 | self.help = help 167 | self.optional = optional 168 | 169 | @property 170 | def data(self): 171 | return {"default": self.default, "min": self.min, "max": self.max, "dtype": self.dtype, "help": self.help, "optional": self.optional} 172 | 173 | @property 174 | def __str__(self): 175 | return "Float" + json.dumps({k: v for k, v in self.data.items() if v is not None}).replace('{','(').replace('}',')').replace(': ','=').replace('"','') 176 | 177 | @property 178 | def __dict__(self): 179 | return {k: v for k, v in self.data.items() if v is not None} 180 | 181 | class List(): #TODO define the types that can be in the list 182 | default = None 183 | help = None 184 | optional = None 185 | 186 | def __init__(self, default=None, help=None, optional=None): 187 | self.default = default 188 | self.help = help 189 | self.optional = optional 190 | 191 | @property 192 | def data(self): 193 | return {"default": self.default, "help": self.help, "optional": self.optional} 194 | 195 | @property 196 | def __str__(self): 197 | return "List" + json.dumps({k: v for k, v in self.data.items() if v is not None}).replace('{','(').replace('}',')').replace(': ','=').replace('"','') 198 | 199 | @property 200 | def __dict__(self): 201 | return {k: v for k, v in self.data.items() if v is not None} 202 | -------------------------------------------------------------------------------- /install_mac.sh: -------------------------------------------------------------------------------- 1 | TMP_DIR="$(mktemp -d)" 2 | if [ -z "$TMP_DIR" ]; then 3 | echo "Failed to create temporary directory" 4 | exit 1 5 | fi 6 | #make sure running with superuser privileges 7 | if [ "$(id -u)" != "0" ]; then 8 | echo "Please run with sudo." 9 | exit 1 10 | fi 11 | 12 | #Define installation paths 13 | user=`logname` 14 | # if user is root, exit 15 | if [ "$user" == "root" ]; then 16 | echo "User is root, please install using sudo in a user and not root account." 17 | exit 1 18 | fi 19 | if [ -z "$user" ]; then 20 | user=$SUDO_USER 21 | fi 22 | if [ -z "$user" ]; then 23 | echo "Failed to get user" 24 | exit 1 25 | fi 26 | user_home=$(dscl . -read /Users/$user NFSHomeDirectory | awk '{print $2}') 27 | if [ -z "$user_home" ]; then 28 | user_home="/Users/$user" 29 | fi 30 | if [ -z "$user_home" ]; then 31 | echo "Failed to get user home" 32 | exit 1 33 | fi 34 | config_path="$user_home/Library/Application Support/DeepMake" 35 | install_path="$user_home/Library/Application Support/DeepMake/DeepMake/" 36 | conda_install_path="$user_home/miniconda3" 37 | aeplugin_path="/Library/Application Support/Adobe/Common/Plug-ins/7.0/MediaCore/" 38 | 39 | #If config_path is inside / then exit 40 | if [[ "$config_path" == /Library/* ]]; then 41 | echo "config_path is in root, exiting to prevent deletion of root files" 42 | echo $config_path 43 | exit 1 44 | fi 45 | if [[ "$install_path" == /Library/* ]]; then 46 | echo "install_path is in root, exiting to prevent deletion of root files" 47 | echo $install_path 48 | exit 1 49 | fi 50 | 51 | mkdir -p "$config_path" 52 | if ! [ -d "$config_path" ]; then 53 | echo "Failed to create $config_path" 54 | exit 1 55 | fi 56 | 57 | mkdir -p "$install_path" 58 | if ! [ -d "$install_path" ]; then 59 | echo "Failed to create $install_path" 60 | exit 1 61 | fi 62 | 63 | 64 | #Create Config file 65 | config_data=`echo '{ "Py_Environment": "conda activate deepmake;", "Startup_CMD": " python startup.py", "Directory": "cd '$install_path' ;" }' | sed 's^Application\ Support^Application\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ Support^'` 66 | echo "$config_data" > "$config_path"/Config.json 67 | 68 | #Install conda if not installed 69 | if command -v conda &> /dev/null; then 70 | echo "conda is installed and available in the PATH" 71 | conda_path="conda" 72 | $conda_path init zsh bash 73 | $conda_path config --set auto_activate_base false 74 | else 75 | conda_path=$conda_install_path"/bin/conda" 76 | echo "conda is not installed or not in the PATH" 77 | curl -s https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-arm64.sh -o $TMP_DIR/miniconda.sh 78 | bash $TMP_DIR/miniconda.sh -b -p $conda_install_path 79 | $conda_path init zsh bash 80 | $conda_path config --set auto_activate_base false 81 | fi 82 | 83 | if ! command -v conda &> /dev/null; then 84 | echo "conda is not installed or not in the PATH" 85 | exit 1 86 | fi 87 | 88 | #Install git from conda if not installed 89 | if ! command -v git &> /dev/null; then 90 | $conda_path install -y git 91 | fi 92 | 93 | if ! command -v git &> /dev/null; then 94 | echo "git is not installed or not in the PATH" 95 | exit 1 96 | fi 97 | 98 | #if install path is empty, clone the repo otherwise pull the latest changes 99 | if [ -z "$(ls -A "$install_path")" ]; then 100 | #Check if install_path is a git folder and if not remove it 101 | if ! [ -d "$install_path/.git" ]; then 102 | echo "Removing non-git folder at $install_path" 103 | rm -Rf "$install_path" 104 | fi 105 | echo "Installing DeepMake to $install_path" 106 | git clone https://github.com/DeepMakeStudio/DeepMake.git "$install_path" 107 | else 108 | echo "Updating DeepMake at $install_path" 109 | cd "$install_path" 110 | git fetch --all 111 | git reset --hard origin/main 112 | git pull -f 113 | cd - 114 | fi 115 | 116 | if ! [ -d "$install_path" ]; then 117 | echo "Failed to install DeepMake to $install_path" 118 | exit 1 119 | fi 120 | 121 | #Create conda environment 122 | $conda_path env update -f "$install_path"/environment.yml 123 | 124 | #Install Diffusers plugin or update if already installed 125 | if [ -z "$(ls -A "$install_path/plugin/Diffusers")" ]; then 126 | echo "Installing Diffusers to $install_path/plugin/Diffusers" 127 | git clone https://github.com/DeepMakeStudio/Diffusers.git "$install_path/plugin/Diffusers" 128 | else 129 | echo "Updating Diffusers Plugin at $install_path/plugin/Diffusers" 130 | cd "$install_path/plugin/Diffusers" 131 | git fetch --all 132 | git reset --hard origin/main 133 | git pull -f 134 | cd - 135 | fi 136 | 137 | if ! [ -d "$install_path/plugin/Diffusers" ]; then 138 | echo "Failed to install Diffusers Plugin to $install_path/plugin/Diffusers" 139 | exit 1 140 | fi 141 | 142 | $conda_path env update -f "$install_path/plugin/Diffusers"/environment_mac.yml 143 | 144 | #Download binaries 145 | curl -s -L --retry 10 --retry-delay 5 https://github.com/DeepMakeStudio/DeepMake/releases/latest/download/Binaries_Mac.zip -o "$TMP_DIR"/Binaries_Mac.zip 146 | if ! [ -f "$TMP_DIR"/Binaries_Mac.zip ]; then 147 | echo "Failed to download Binaries_Mac.zip" 148 | exit 1 149 | fi 150 | 151 | if [ -z "$(ls -A "$TMP_DIR")" ]; then 152 | echo "Failed to download Binaries_Mac.zip" 153 | exit 1 154 | fi 155 | 156 | #if the plugin path does not exist, create it 157 | if ! [ -d "$aeplugin_path" ]; then 158 | echo "Warning, $aeplugin_path does not exist, creating it (Did you install After Effects?)" 159 | mkdir -p "$aeplugin_path" 160 | fi 161 | 162 | #unzip and error if failed 163 | unzip -o "$TMP_DIR"/Binaries_Mac.zip -d "$TMP_DIR" > /dev/null 164 | if [ $? -ne 0 ]; then 165 | echo "Failed to unzip Binaries_Mac.zip" 166 | exit 1 167 | fi 168 | 169 | # if /DeepMake/DeepMake_ae.bundle exists, remove it 170 | if [ -e "$aeplugin_path"/DeepMake_ae.bundle ]; then 171 | echo "Removing existing DeepMake_ae.bundle at $aeplugin_path" 172 | fi 173 | cp -Rf "$TMP_DIR"/DeepMake/DeepMake_ae.bundle "$aeplugin_path" 174 | 175 | if [ ! -d "$aeplugin_path"/DeepMake_ae.bundle ]; then 176 | echo "Failed to install DeepMake_ae.bundle to $aeplugin_path" 177 | exit 1 178 | fi 179 | 180 | # if /Applications/appPrompt.app exists, remove it 181 | if [ -e /Applications/appPrompt.app ]; then 182 | echo "Removing existing appPrompt.app at /Applications" 183 | rm -Rf /Applications/appPrompt.app 184 | fi 185 | cp -Rf "$TMP_DIR"/DeepMake/appPrompt.app /Applications/appPrompt.app 186 | 187 | if [ ! -d /Applications/appPrompt.app ]; then 188 | echo "Failed to install appPrompt.app to /Applications" 189 | exit 1 190 | fi 191 | 192 | rm -Rf $TMP_DIR 193 | 194 | #Change ownership of config_path and install_path 195 | chown -R $user "$config_path" 196 | 197 | #if ownership of config_path is not the user, exit 198 | if [ "$(stat -f %Su "$config_path" | head -n1)" != "$user" ]; then 199 | echo "Failed to change ownership of $config_path to $user" 200 | echo "is $(stat -f %Su "$config_path") should be $user" 201 | exit 1 202 | fi 203 | 204 | chown -R $user "$install_path" 205 | 206 | #if ownership of install_path is not the user, exit 207 | if [ "$(stat -f %Su "$install_path" | head -n1)" != "$user" ]; then 208 | echo "Failed to change ownership of $install_path to $user" 209 | echo "is $(stat -f %Su "$install_path") should be $user" 210 | exit 1 211 | fi 212 | 213 | open "https://deepmake.com/postinstall/" 214 | 215 | ~/anaconda3/bin/python "$install_path"/finalize_install.py 216 | 217 | echo "Installation complete." -------------------------------------------------------------------------------- /plugin/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import HTTPException 2 | import uuid 3 | import requests 4 | from huey.storage import FileStorage, SqliteStorage 5 | import huey 6 | import os 7 | import sys 8 | import numpy as np 9 | from io import BytesIO 10 | from PIL import Image 11 | from tqdm import tqdm 12 | import shutil 13 | import threading 14 | import storage_db 15 | 16 | if sys.platform == "win32": 17 | storage_folder = os.path.join(os.getenv('APPDATA'),"DeepMake") 18 | elif sys.platform == "darwin": 19 | storage_folder = os.path.join(os.getenv('HOME'),"Library","Application Support","DeepMake") 20 | elif sys.platform == "linux": 21 | storage_folder = os.path.join(os.getenv('HOME'),".local", "DeepMake") 22 | 23 | if not os.path.exists(storage_folder): 24 | os.mkdir(storage_folder) 25 | 26 | storage = SqliteStorage(name="storage", filename=os.path.join(storage_folder, 'huey_storage.db')) 27 | print(storage_folder) 28 | 29 | def fetch_image(img_id): 30 | img_data = storage.peek_data(img_id) 31 | if img_data == huey.constants.EmptyData: 32 | # print("No image found for id", img_id) 33 | raise HTTPException(status_code=400, detail=f"No image found for id {img_id}") 34 | return img_data 35 | 36 | def fetch_pil_image(img_id): 37 | img_data = fetch_image(img_id) 38 | return Image.open(BytesIO(img_data)) 39 | 40 | def store_pil_image(img, img_id=None): 41 | output = BytesIO() 42 | img.save(output, format="PNG") 43 | img_data = output.getvalue() 44 | return store_image(img_data, img_id) 45 | 46 | def store_image(img_data, img_id=None): 47 | if img_id is None: 48 | img_id = str(uuid.uuid4()) 49 | if not isinstance(img_data, bytes): 50 | raise HTTPException(status_code=400, detail=f"Data must be stored in bytes") 51 | storage.put_data(img_id, img_data) 52 | return img_id 53 | 54 | def store_multiple_images(img_data): 55 | video_id = str(uuid.uuid4()) 56 | 57 | img_id_list = [store_image(img_data[i], video_id + f"_{i}") for i in range(len(img_data))] 58 | bytes_list = bytes(";".join(img_id_list).encode("utf-8")) 59 | storage.put_data(video_id, bytes_list) 60 | return video_id 61 | 62 | def store_multiple(data_list, func, img_ids=None): 63 | list_id = str(uuid.uuid4()) 64 | if img_ids is None: 65 | img_ids = [func(img) for img in data_list] 66 | elif len(data_list) == len(img_ids): 67 | img_ids = [func(img, img_id) for img, img_id in zip(data_list, img_ids)] 68 | elif type(img_ids) == str: 69 | img_ids = [func(img, img_ids + str(i)) for i, img in enumerate(data_list)] 70 | bytes_list = bytes(";".join(img_ids).encode("utf-8")) 71 | storage.put_data(list_id, bytes_list) 72 | return list_id 73 | 74 | def fetch_multiple(func, id_list): 75 | return [func(img_id) for img_id in id_list] 76 | 77 | class Plugin(): 78 | """ 79 | Generic plugin class 80 | """ 81 | 82 | def __init__(self, arguments={}, plugin_name="default"): 83 | self.plugin_name = plugin_name 84 | self.db = storage_db.storage_db() 85 | if arguments == {}: 86 | self.plugin = {} 87 | self.config = {} 88 | self.endpoints = {} 89 | else: 90 | self.plugin = arguments.plugin 91 | self.config = arguments.config 92 | self.endpoints = arguments.endpoints 93 | config = self.db.retrieve_data(f"plugin_config.{self.plugin_name}") 94 | if config: 95 | self.config = config 96 | 97 | # Create a plugin-specific storage path 98 | self.plugin_storage_path = os.path.join(storage_folder, self.plugin_name) 99 | os.makedirs(self.plugin_storage_path, exist_ok=True) 100 | 101 | def plugin_info(self): 102 | """ int: The number of samples to take from the input video/images """ 103 | return {"plugin": self.plugin,"config": self.config, "endpoints": self.endpoints} 104 | 105 | def get_config(self): 106 | return self.config 107 | 108 | def set_config(self, update: dict): 109 | self.config.update(update) # TODO: Validate config dict are all valid keys 110 | self.db.store_data(f"plugin_config.{self.plugin_name}", self.config) 111 | if "model_name" in update: 112 | self.set_model() 113 | return self.config 114 | 115 | def progress_callback(self, progress, stage): 116 | if progress >= 0: 117 | print(f"{stage}: {progress*100}% complete") 118 | else: 119 | print(f"{stage}: Progress not available") 120 | 121 | 122 | def download_model(self, model_url, save_path, progress_callback=None): 123 | """Download a model with a progress bar and report progress in 5% increments.""" 124 | resp = requests.get(model_url, stream=True) 125 | total_size = int(resp.headers.get('content-length', 0)) 126 | downloaded_size = 0 127 | last_reported_progress = -0.05 # Initialize to -5% so the first callback happens at 0% 128 | 129 | with open(save_path, 'wb') as file: 130 | for data in resp.iter_content(chunk_size=1024): 131 | file.write(data) 132 | downloaded_size += len(data) 133 | progress = downloaded_size / total_size 134 | if progress >= last_reported_progress + 0.05: # Check if we've advanced by another 5% 135 | last_reported_progress = progress 136 | if progress_callback: 137 | progress_callback(progress, f"Downloading {os.path.basename(save_path)}") 138 | 139 | if progress_callback: 140 | progress_callback(1.0, f"Downloading {os.path.basename(save_path)} complete") 141 | 142 | def on_install(self, model_urls, progress_callback=None): 143 | """Install necessary models for the plugin and report detailed progress in 5% increments.""" 144 | try: 145 | total_models = len(model_urls) 146 | current_model_index = 0 147 | 148 | for model_name, model_url in model_urls.items(): 149 | model_path = os.path.join(self.plugin_storage_path, model_name) 150 | if not os.path.exists(model_path): 151 | def model_progress_callback(local_progress, stage): 152 | # Calculate the overall progress based on the current model index and local progress 153 | overall_progress = ((current_model_index + local_progress) / total_models) 154 | # Convert overall progress to percentage 155 | progress_percentage = round(overall_progress * 100) 156 | self.notify_main_system_of_installation_async(progress_percentage, stage) 157 | 158 | print(f"Downloading {model_name}...") 159 | self.download_model(model_url, model_path, model_progress_callback) 160 | else: 161 | print(f"{model_name} already exists.") 162 | 163 | # After each model is processed (downloaded or skipped), increment the current_model_index 164 | current_model_index += 1 165 | # Notify for the completion of this model's installation 166 | self.notify_main_system_of_installation_async(round((current_model_index / total_models) * 100), f"{model_name} installation complete") 167 | 168 | # After all models are processed, notify completion 169 | self.notify_main_system_of_installation_async(100, "Installation complete") 170 | except Exception as e: 171 | self.notify_main_system_of_installation_async(-1, f"Installation failed: {str(e)}") 172 | print(f"Error installing resources for {self.plugin_name}: {str(e)}") 173 | 174 | def on_uninstall(self, progress_callback=None): 175 | """Clean up resources used by the plugin.""" 176 | try: 177 | shutil.rmtree(self.plugin_storage_path) 178 | if progress_callback: 179 | progress_callback(1.0, "Uninstallation complete") 180 | print(f"Removed all resources for {self.plugin_name}.") 181 | except Exception as e: 182 | if progress_callback: 183 | progress_callback(-1, f"Uninstallation failed: {str(e)}") 184 | print(f"Error removing resources for {self.plugin_name}: {str(e)}") 185 | 186 | def notify_main_system_of_startup(self, status: str): 187 | callback_url = f"http://localhost:8000/plugin_callback/{self.plugin_name}/{status}" 188 | try: 189 | response = requests.post(callback_url) 190 | print("Response from main system:", response.json()) # Print the response content 191 | if response.json()["status"] == "success": 192 | print("Callback successful. Plugin is now in RUNNING state.") 193 | else: 194 | print("Callback failed. Check the main system.") 195 | except: 196 | print("Failed to notify the main system. Ensure it's running.") 197 | 198 | def notify_main_system_of_installation(self, progress, stage): 199 | # Ensure progress is rounded to the nearest whole number for reporting 200 | print(progress, stage) 201 | #progress_percentage = round(progress * 100) 202 | callback_url = f"http://localhost:8000/plugin_install_callback/{self.plugin_name}/{progress}/{stage.replace(' ', '%20')}" 203 | try: 204 | response = requests.post(callback_url) 205 | print(f"Installation progress update to main system: {progress}% complete. Current stage: {stage}") 206 | except Exception as e: 207 | print("Failed to notify the main system of installation progress:", e) 208 | 209 | def notify_main_system_of_uninstallation(self, progress, stage): 210 | callback_url = f"http://localhost:8000/plugin_uninstall_callback/{self.plugin_name}/{progress}/{stage}" 211 | try: 212 | response = requests.post(callback_url) 213 | print("Uninstallation progress update to main system:", response.json()) 214 | except Exception as e: 215 | print("Failed to notify the main system of uninstallation progress:", e) 216 | 217 | def notify_main_system_of_installation_async(self, progress, stage): 218 | threading.Thread(target=self.notify_main_system_of_installation, args=(progress, stage)).start() 219 | 220 | 221 | -------------------------------------------------------------------------------- /pyqt_gui_table.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtWidgets import QApplication, QWidget, QPushButton, QProgressBar, QComboBox, QHBoxLayout, QListWidget, QHeaderView, QTableWidget, QVBoxLayout, QTableWidgetItem, QDialog, QScrollArea, QDialogButtonBox 2 | from PyQt6.QtCore import Qt, pyqtSignal, QObject, QThread 3 | 4 | import os 5 | import json 6 | import subprocess 7 | fastapi_launcher_path = os.path.join(os.path.dirname(__file__), "plugin") 8 | import sys 9 | import requests 10 | 11 | client = requests.Session() 12 | 13 | class Worker(QObject): 14 | popen_string = " " 15 | finished = pyqtSignal() 16 | # progress = pyqtSignal(int) 17 | def run(self): 18 | """Long-running task.""" 19 | if sys.platform != "win32": 20 | p = subprocess.Popen(self.popen_string.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE) 21 | else: 22 | p = subprocess.Popen(self.popen_string, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 23 | p.wait() 24 | 25 | self.finished.emit() 26 | 27 | class PluginManager(QWidget): 28 | def __init__(self): 29 | super().__init__() 30 | self.title = "Plugin Manager" 31 | self.left = 0 32 | self.top = 0 33 | self.width = 1000 34 | self.height = 300 35 | self.setWindowTitle(self.title) 36 | self.setGeometry(self.left, self.top, self.width, self.height) 37 | # self.setStyleSheet( "color: white; border-color: #7b3bff") 38 | r = client.get(f"http://127.0.0.1:8000/plugins/get_config/Diffusers") 39 | 40 | 41 | with open(os.path.join(os.path.dirname(__file__), "gui_info.json")) as f: 42 | self.plugin_dict = json.load(f) 43 | self.createTable() 44 | 45 | self.scrollArea = QScrollArea() 46 | self.scrollArea.setWidget(self.tableWidget) 47 | # compute the correct minimum width 48 | width = (self.tableWidget.sizeHint().width() + 49 | self.scrollArea.verticalScrollBar().sizeHint().width() + 50 | self.scrollArea.frameWidth() * 2) 51 | self.scrollArea.setMinimumWidth(width) 52 | 53 | self.layout = QVBoxLayout() 54 | self.layout.addWidget(self.tableWidget) 55 | self.setLayout(self.layout) 56 | # self.setFixedSize(self.layout.sizeHint()) 57 | 58 | 59 | def createTable(self): 60 | self.tableWidget = QTableWidget() 61 | row_count = len(self.plugin_dict["plugin"]) 62 | column_count = 5 63 | 64 | self.tableWidget.setRowCount(row_count) 65 | self.tableWidget.setColumnCount(column_count) 66 | self.button_dict = {} 67 | 68 | for row in range(row_count): 69 | plugin_name = list(self.plugin_dict["plugin"].keys())[row] 70 | for col in range(column_count): 71 | if col == 0: 72 | self.tableWidget.setItem(row, col, QTableWidgetItem(plugin_name)) 73 | elif col == 1: 74 | self.tableWidget.setItem(row, col, QTableWidgetItem(self.plugin_dict["plugin"][plugin_name]["Description"])) 75 | elif col == 2: 76 | item = QTableWidgetItem(self.plugin_dict["plugin"][plugin_name]["Version"]) 77 | self.tableWidget.setItem(row, col, item) 78 | item.setTextAlignment(Qt.AlignmentFlag.AlignHCenter) 79 | elif col == 3: 80 | if plugin_name in os.listdir(fastapi_launcher_path): 81 | self.Installed(plugin_name) 82 | else: 83 | self.install_button_creation(plugin_name) 84 | elif col == 4: 85 | if plugin_name in os.listdir(fastapi_launcher_path): 86 | button = QPushButton(f"Manage") 87 | button.clicked.connect(lambda _, name = plugin_name: self.uninstall_plugin(name)) 88 | self.button_dict[plugin_name] = button 89 | self.tableWidget.setCellWidget(row, col, button) 90 | else: 91 | self.manage(plugin_name) 92 | 93 | 94 | 95 | self.tableWidget.setShowGrid(False) 96 | self.tableWidget.setHorizontalHeaderLabels(['Name', 'Description', 'Version', 'Install', "Manage"]) 97 | self.tableWidget.verticalHeader().setVisible(False) 98 | self.tableWidget.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) 99 | self.tableWidget.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) 100 | self.tableWidget.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) 101 | self.tableWidget.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch) 102 | self.tableWidget.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeMode.Stretch) 103 | 104 | def install_button_creation(self, plugin_name): 105 | row = list(self.plugin_dict['plugin'].keys()).index(plugin_name) 106 | button = QPushButton(f"Install") 107 | button.clicked.connect(lambda _, name = plugin_name: self.install_plugin(name)) 108 | self.button_dict[plugin_name] = button 109 | self.tableWidget.setItem(row, 3, QTableWidgetItem(" ")) 110 | self.tableWidget.setCellWidget(row, 3, button) 111 | 112 | def install_plugin(self, plugin_name): 113 | if plugin_name in os.listdir(fastapi_launcher_path): 114 | print("Plugin already installed") 115 | return 116 | 117 | row_number = list(self.plugin_dict['plugin'].keys()).index(plugin_name) 118 | # installing_button = QPushButton(f"Installing") 119 | self.tableWidget.removeCellWidget(row_number, 3) 120 | 121 | installing_item = QTableWidgetItem("Installing...") 122 | self.tableWidget.setItem(row_number, 3, installing_item) 123 | installing_item.setTextAlignment(Qt.AlignmentFlag.AlignHCenter) 124 | clone_link = self.plugin_dict["plugin"][plugin_name]["url"] + ".git" 125 | folder_path = os.path.join(os.path.dirname(__file__), "plugin", plugin_name) 126 | print("Installing", plugin_name) 127 | r = client.get(f"http://127.0.0.1:8000/") 128 | print(r.text) 129 | 130 | self.thread_process(f"conda env create -f {folder_path}/environment.yml", row_number) 131 | 132 | def uninstall_plugin(self, plugin_name): 133 | if plugin_name not in os.listdir(fastapi_launcher_path): 134 | print("Plugin not installed") 135 | return 136 | 137 | dlg = CustomDialog(plugin_name) 138 | if dlg.exec(): 139 | print("Uninstalling", plugin_name) 140 | folder_path = os.path.join(os.path.dirname(__file__), "plugin", plugin_name) 141 | if sys.platform != "win32": 142 | p = subprocess.Popen(f"rm -rf {folder_path}".split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE) 143 | else: 144 | p = subprocess.Popen(f"rm -rf {folder_path}", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 145 | self.button_dict.pop(plugin_name) 146 | self.tableWidget.removeCellWidget(list(self.plugin_dict['plugin'].keys()).index(plugin_name), 2) 147 | self.install_button_creation(plugin_name) 148 | self.manage(plugin_name) 149 | 150 | def manage(self, plugin_name): 151 | row = list(self.plugin_dict['plugin'].keys()).index(plugin_name) 152 | item = QTableWidgetItem("Install First!") 153 | item.setTextAlignment(Qt.AlignmentFlag.AlignHCenter) 154 | self.tableWidget.removeCellWidget(row, 4) 155 | self.tableWidget.setItem(row, 4, item) 156 | 157 | 158 | def uninstall_button_creation(self, plugin_name): 159 | row = list(self.plugin_dict['plugin'].keys()).index(plugin_name) 160 | button = QPushButton(f"Manage") 161 | button.clicked.connect(lambda _, name = plugin_name: self.uninstall_plugin(name)) 162 | self.button_dict[plugin_name] = button 163 | self.tableWidget.setItem(row, 4, QTableWidgetItem(" ")) 164 | self.tableWidget.setCellWidget(row, 4, button) 165 | print("Finish env", plugin_name) 166 | 167 | def thread_process(self, popen_string, row_number): 168 | 169 | self.thread = QThread() 170 | 171 | self.worker = Worker() 172 | self.worker.popen_string = popen_string 173 | 174 | self.worker.moveToThread(self.thread) 175 | self.thread.started.connect(self.worker.run) 176 | self.worker.finished.connect(self.thread.quit) 177 | self.worker.finished.connect(self.worker.deleteLater) 178 | self.thread.finished.connect(self.thread.deleteLater) 179 | 180 | self.thread.start() 181 | plugin_name = list(self.plugin_dict["plugin"].keys())[row_number] 182 | self.thread.finished.connect(lambda: self.uninstall_button_creation(plugin_name)) 183 | self.thread.finished.connect(lambda: self.Installed(plugin_name)) 184 | 185 | def Installed(self, plugin_name): 186 | row = list(self.plugin_dict['plugin'].keys()).index(plugin_name) 187 | install_label = QTableWidgetItem("Installed") 188 | install_label.setTextAlignment(Qt.AlignmentFlag.AlignHCenter) 189 | self.tableWidget.setItem(row, 3, install_label) 190 | 191 | 192 | class CustomDialog(QDialog): 193 | def __init__(self, plugin_name): 194 | super().__init__() 195 | self.name = plugin_name 196 | self.setWindowTitle("Manage") 197 | self.setGeometry(0, 0, 500, 200) 198 | 199 | 200 | self.buttonBox = QDialogButtonBox() 201 | self.buttonBox.addButton("Uninstall", QDialogButtonBox.ButtonRole.AcceptRole) 202 | # self.buttonBox.addButton("Update to Latest", QDialogButtonBox.ButtonRole.RejectRole) 203 | self.buttonBox.accepted.connect(self.accept) 204 | # self.buttonBox.centerButtons() 205 | # uninstall_button = QPushButton() 206 | # uninstall_button.addButton("Uninstall", QDialogButtonBox.ButtonRole.AcceptRole) 207 | self.layout = QVBoxLayout() 208 | self.createTable() 209 | self.update_button = QPushButton("Update to Latest") 210 | self.update_button.clicked.connect(lambda: self.update_plugin(self.name)) 211 | # self.test_button = QPushButton("Test") 212 | self.tableWidget.setCellWidget(1,0, self.buttonBox) 213 | self.tableWidget.setCellWidget(1, 2, self.update_button) 214 | # self.tableWidget.setCellWidget(1, 1,self.test_button) 215 | # self.test_button.clicked.connect(self.test_plugin) 216 | # message = QLabel("Do you want to uninstall?") 217 | # self.layout.addWidget(message) 218 | self.layout.addWidget(self.tableWidget) 219 | # self.layout.addWidget(self.buttonBox) 220 | self.setLayout(self.layout) 221 | 222 | def test_plugin(self): 223 | self.tableWidget.setItem(0, 1, QTableWidgetItem("Testing...")) 224 | 225 | 226 | def update_plugin(self, plugin_name): 227 | if len(self.tag_list) == 0: 228 | print("No versioning") 229 | return 230 | version = self.update_button.text().split()[-1] 231 | if version == "Latest": 232 | version = self.tag_list[0] 233 | print("Updating", plugin_name, "to version", version) 234 | 235 | origin_folder = os.path.dirname(__file__) 236 | os.chdir(os.path.join(origin_folder, "plugin", plugin_name)) 237 | p = subprocess.Popen(f"git checkout {version} ".split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE) 238 | p.wait() 239 | # print(p.communicate()) 240 | os.chdir(origin_folder) 241 | # current_tag = self.getVersion() 242 | self.tableWidget.setItem(0, 0, QTableWidgetItem(f"Current Version: {version}")) 243 | print("Updated", plugin_name, "to version", version) 244 | 245 | def version_management(self): 246 | tag_list = [] 247 | origin_folder = os.path.dirname(__file__) 248 | os.chdir(os.path.join(origin_folder, "plugin", self.name)) 249 | tags = subprocess.check_output("git tag".split()).decode("utf-8") 250 | os.chdir(origin_folder) 251 | for label in tags.split("\n")[:-1]: 252 | tag_list.append(label) 253 | return tag_list 254 | 255 | def createTable(self): 256 | self.tableWidget = QTableWidget() 257 | row_count = 2 258 | column_count = 3 259 | 260 | self.tableWidget.setRowCount(row_count) 261 | self.tableWidget.setColumnCount(column_count) 262 | self.button_dict = {} 263 | current_tag = self.getVersion() 264 | self.tableWidget.setItem(0, 0, QTableWidgetItem(f"Current Version: {current_tag}")) 265 | self.tableWidget.setItem(0, 1, QTableWidgetItem("Available Versions")) 266 | dropdown = QComboBox() 267 | self.tag_list = self.version_management() 268 | self.tag_list.reverse() 269 | dropdown.addItems(self.tag_list) 270 | dropdown.currentTextChanged.connect(self.changeVersion) 271 | 272 | self.tableWidget.setCellWidget(0, 2, dropdown) 273 | 274 | self.tableWidget.setShowGrid(False) 275 | self.tableWidget.horizontalHeader().setVisible(False) 276 | self.tableWidget.verticalHeader().setVisible(False) 277 | # self.tableWidget.setHorizontalHeaderLabels(['Name', 'Description', 'Version', 'Install']) 278 | 279 | self.tableWidget.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) 280 | self.tableWidget.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) 281 | self.tableWidget.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) 282 | def changeVersion(self, text): 283 | self.update_button.setText(f"Update to {text}") 284 | 285 | def getVersion(self): 286 | origin_folder = os.path.join(fastapi_launcher_path, self.name) 287 | os.chdir(origin_folder) 288 | try: 289 | tag = subprocess.check_output("git describe --tags".split()).decode("utf-8").split("\n")[0] 290 | except: 291 | tag = "0.0.0" 292 | os.chdir(os.path.dirname(__file__)) 293 | # print(tag) 294 | return tag -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, HTTPException, File, UploadFile, Response, Request 2 | from fastapi.responses import RedirectResponse 3 | from typing import Optional, List 4 | from pydantic import BaseModel 5 | from datetime import datetime 6 | import shutil 7 | import time 8 | import re 9 | from PIL import Image 10 | from io import BytesIO 11 | from auth_handler import auth_handler as auth 12 | from plugin import Plugin 13 | 14 | import os 15 | import base64 16 | import uuid 17 | import json 18 | import psutil 19 | from psutil import NoSuchProcess 20 | import requests 21 | import subprocess 22 | import numpy as np 23 | import sys 24 | import importlib 25 | from time import sleep 26 | from huey import SqliteHuey 27 | from huey.storage import SqliteStorage 28 | from huey.constants import EmptyData 29 | from huey.exceptions import TaskException 30 | import sentry_sdk 31 | from sentry_sdk.integrations.huey import HueyIntegration 32 | from hashlib import md5 33 | import sqlite3 34 | from PySide6.QtWidgets import QApplication 35 | 36 | from routers import ui, plugin_manager, report, login 37 | 38 | import asyncio 39 | from huey.exceptions import TaskException 40 | from fastapi import Depends 41 | from fastapi.middleware.cors import CORSMiddleware 42 | 43 | origins = [ 44 | "http://deepmake.com", 45 | "https://deepmake.com", 46 | "http://localhost", 47 | "http://localhost:8080", 48 | "http://127.0.0.1", 49 | "http://127.0.0.1:8080", 50 | ] 51 | 52 | origin_regexes = [ 53 | "http://.*\.deepmake\.com", 54 | "https://.*\.deepmake\.com", 55 | ] 56 | 57 | CONDA = "MiniConda3" 58 | 59 | def get_id(): # return md5 hash of uuid.getnode() 60 | return md5(str(uuid.getnode()).encode()).hexdigest() 61 | 62 | sentry_sdk.init( 63 | dsn="https://d4853d3e3873643fa675bc620a58772c@o4506430643175424.ingest.sentry.io/4506463076614144", 64 | traces_sample_rate=0.1, 65 | profiles_sample_rate=0.1, 66 | enable_tracing=True, 67 | integrations=[ 68 | HueyIntegration(), 69 | ], 70 | ) 71 | user = {"id": get_id()} 72 | sentry_sdk.set_tag("platform", sys.platform) 73 | sentry_sdk.set_tag("os", sys.platform) 74 | if auth.logged_in: 75 | user["email"] = auth.username 76 | user_info = auth.get_user_info() 77 | if "id" in user_info.keys(): 78 | user["acct_id"] = user_info["id"] 79 | sentry_sdk.set_user(user) 80 | 81 | sentry_sdk.capture_message('Backend started') 82 | 83 | global port_mapping 84 | global plugin_endpoints 85 | global storage_dictionary 86 | 87 | if sys.platform == "win32": 88 | storage_folder = os.path.join(os.getenv('APPDATA'),"DeepMake") 89 | elif sys.platform == "darwin": 90 | storage_folder = os.path.join(os.getenv('HOME'),"Library","Application Support","DeepMake") 91 | elif sys.platform == "linux": 92 | storage_folder = os.path.join(os.getenv('HOME'),".local", "DeepMake") 93 | # exit() 94 | 95 | if not os.path.exists(storage_folder): 96 | os.mkdir(storage_folder) 97 | 98 | storage = SqliteStorage(name="storage", filename=os.path.join(storage_folder, 'huey_storage.db')) 99 | 100 | huey = SqliteHuey(filename=os.path.join(storage_folder,'huey.db')) 101 | 102 | app = FastAPI() 103 | 104 | app.add_middleware( 105 | CORSMiddleware, 106 | allow_origins=origins, 107 | allow_credentials=True, 108 | allow_methods=["*"], 109 | allow_headers=["*"], 110 | ) 111 | 112 | app.include_router(ui.router, tags=["ui"], prefix="/ui") 113 | app.include_router(plugin_manager.router, tags=["plugin_manager"], prefix="/plugin_manager") 114 | app.include_router(report.router, tags=["report"], prefix="/report") 115 | app.include_router(login.router, tags=["login"], prefix="/login") 116 | client = requests.Session() 117 | 118 | class Task(BaseModel): 119 | id: str 120 | name: str 121 | description: Optional[str] = None 122 | 123 | class Job(BaseModel): 124 | id: str 125 | task: Task 126 | status: str = 'queued' 127 | created_at: datetime = datetime.now() 128 | description: Optional[str] = None 129 | 130 | jobs = {} 131 | finished_jobs = [] 132 | running_jobs = [] 133 | jobs = {} 134 | most_recent_use = [] 135 | 136 | plugin_info = {} 137 | 138 | port_mapping = {"main": 8000} 139 | process_ids = {} 140 | plugin_endpoints = {} 141 | plugin_memory = {} 142 | PLUGINS_DIRECTORY = "plugin" 143 | 144 | def fetch_image(img_id): 145 | img_data = storage.peek_data(img_id) 146 | if img_data == EmptyData: 147 | raise HTTPException(status_code=400, detail=f"No image found for id {img_id}") 148 | return img_data 149 | 150 | @app.get("/get_main_pid/{pid}") 151 | def get_main_pid(pid): 152 | if "main" in process_ids: 153 | return {"status": "failed", "error": "Already received a pid"} 154 | process_ids["main"] = int(pid) 155 | return {"status": "success"} 156 | 157 | @app.on_event("startup") 158 | def startup(): 159 | reload_plugin_list() 160 | init_db() # Initialize the database 161 | 162 | if sys.platform != "win32": 163 | p = subprocess.Popen("huey_consumer.py main.huey".split()) 164 | else: 165 | huey_script_path = os.path.join(os.path.dirname(sys.executable), "Scripts\\huey_consumer.py") 166 | p = subprocess.Popen([sys.executable, huey_script_path, "main.huey"], shell=True) 167 | pid = p.pid 168 | 169 | process_ids["huey"] = pid 170 | 171 | def init_db(): 172 | conn = sqlite3.connect(os.path.join(storage_folder, 'data_storage.db')) 173 | cursor = conn.cursor() 174 | cursor.execute(""" 175 | CREATE TABLE IF NOT EXISTS key_value_store ( 176 | key TEXT PRIMARY KEY, 177 | value TEXT 178 | ) 179 | """) 180 | conn.commit() 181 | conn.close() 182 | 183 | def reload_plugin_list(): 184 | if "plugin_list" in globals(): 185 | del globals()["plugin_list"] 186 | del globals()["plugin_states"] 187 | global plugin_list 188 | plugin_list=[] 189 | global plugin_states 190 | plugin_states = {} 191 | for folder in os.listdir(PLUGINS_DIRECTORY): 192 | # print(folder) 193 | if os.path.isdir(os.path.join(PLUGINS_DIRECTORY, folder)): 194 | if folder in plugin_list: 195 | pass 196 | elif "plugin.py" not in os.listdir(os.path.join(PLUGINS_DIRECTORY, folder)): 197 | pass 198 | else: 199 | plugin_list.append(folder) 200 | if folder not in plugin_states: 201 | plugin_states[folder] = "INIT" 202 | # print(plugin_list) 203 | for plugin in list(plugin_states.keys()): 204 | if plugin not in plugin_list: 205 | if plugin in process_ids.keys(): 206 | stop_plugin(plugin) 207 | del plugin_states[plugin] 208 | 209 | async def serialize_image(image): 210 | image= base64.b64encode(await image.read()) 211 | img_data = image.decode() 212 | return img_data 213 | 214 | async def store_image(data): 215 | img_data = await data.read() 216 | img_id = str(uuid.uuid4()) 217 | storage.put_data(img_id,img_data) 218 | return img_id 219 | 220 | def available_gpu_memory(): 221 | command = "nvidia-smi --query-gpu=memory.free --format=csv" 222 | try: 223 | memory_free_info = subprocess.check_output(command.split()).decode('ascii').split('\n')[:-1][1:] 224 | except: 225 | return -1 226 | memory_free_values = [int(x.split()[0]) for i, x in enumerate(memory_free_info)] 227 | return np.sum(memory_free_values) 228 | 229 | def mac_gpu_memory(): 230 | vm = subprocess.Popen(['vm_stat'], stdout=subprocess.PIPE).communicate()[0].decode() 231 | vmLines = vm.split('\n') 232 | sep = re.compile(':[\s]+') 233 | vmStats = {} 234 | page_size = int(vmLines[0].split(" ")[-2]) 235 | for row in range(1,len(vmLines)-2): 236 | rowText = vmLines[row].strip() 237 | rowElements = sep.split(rowText) 238 | vmStats[(rowElements[0])] = int(rowElements[1].strip("\.")) * page_size /1024**2 239 | return vmStats["Pages free"] + vmStats["Pages inactive"] 240 | 241 | def new_job(job): 242 | jobs[job.id] = job 243 | running_jobs.append(job.id) 244 | 245 | @app.get("/") 246 | async def redirect_docs(): 247 | return RedirectResponse("/docs/") 248 | 249 | @app.get("/plugins/reload") 250 | async def reload_plugins(): 251 | reload_plugin_list() 252 | return {"status": "success"} 253 | 254 | @app.get("/plugins/get_list") 255 | def get_plugin_list(): 256 | return {"plugins": list(plugin_states)} 257 | 258 | @app.get("/plugins/get_states") 259 | def get_plugin_list(): 260 | result = {} 261 | for plugin in plugin_states.keys(): 262 | if plugin in port_mapping.keys(): 263 | result[plugin] = {"state": plugin_states[plugin], "port": port_mapping[plugin]} 264 | else: 265 | result[plugin] = {"state": plugin_states[plugin]} 266 | return result 267 | 268 | @app.get("/plugins/get_info/{plugin_name}") 269 | def get_plugin_info(plugin_name: str): 270 | try: 271 | r = auth.get_url("https://deepmake.com/plugins.json") 272 | store_data("plugin_info", r) 273 | except Exception as e: 274 | try: 275 | r = retrieve_data("plugin_info") 276 | # print("Can't connect to Internet, using cached file") 277 | except: 278 | r = {} 279 | 280 | json_exists = True 281 | try: 282 | r[plugin_name] 283 | except: 284 | json_exists = False 285 | 286 | if plugin_name in plugin_list: 287 | if plugin_name not in plugin_info.keys(): 288 | plugin = importlib.import_module(f"plugin.{plugin_name}.config", package = f'{plugin_name}.config') 289 | plugin_info[plugin_name] = {"plugin": plugin.plugin, "config": plugin.config, "endpoints": plugin.endpoints} 290 | plugin_endpoints[plugin_name] = plugin.endpoints 291 | # print(plugin_info[plugin_name]["plugin"]["memory"]) 292 | if json_exists: 293 | initial_value = int(r[plugin_name]["vram"].split(" ")[0]) 294 | mult = 1 295 | if "GB" in r[plugin_name]["vram"]: 296 | mult = 1024 297 | initial_value *= mult 298 | else: 299 | initial_value = 1000 300 | store_data(f"{plugin_name}_memory", {"memory": [initial_value]}) 301 | # store_data(f"{plugin_name}_model_memory", {"memory": plugin_info[plugin_name]["plugin"]["model_memory"]}) 302 | store_data(f"{plugin_name}_memory_mean", {"memory": initial_value}) 303 | store_data(f"{plugin_name}_memory_max", {"memory": initial_value}) 304 | store_data(f"{plugin_name}_memory_min", {"memory": initial_value}) 305 | 306 | try: 307 | plugin_info[plugin_name]["plugin"]["license"] = r[plugin_name]["license"] 308 | except: 309 | plugin_info[plugin_name]["plugin"]["license"] = 1 # Unknown plugins require a subscription to run 310 | return plugin_info[plugin_name] 311 | else: 312 | raise HTTPException(status_code=404, detail="Plugin not found") 313 | 314 | @app.get("/plugins/get_config/{plugin_name}") 315 | def get_plugin_config(plugin_name: str): 316 | if plugin_name in plugin_list: 317 | sleep = 0 318 | # while plugin_states[plugin_name] != "RUNNING": 319 | # start_plugin(plugin_name) 320 | # time.sleep(5) 321 | # sleep += 5 322 | # if sleep > 120: 323 | # return {"status": "failed", "error": "Plugin too slow to start"} 324 | if plugin_name in port_mapping.keys(): 325 | port = port_mapping[plugin_name] 326 | r = client.get("http://127.0.0.1:" + port + "/get_config") 327 | if r.status_code == 200: 328 | return r.json() 329 | else: 330 | return {"status": "failed", "error": r.text} 331 | else: 332 | try: 333 | # print(f"Getting config for {plugin_name}") 334 | # print(f"plugin_config.{plugin_name}") 335 | data = retrieve_data(f"plugin_config.{plugin_name}") 336 | # print(data) 337 | return data 338 | except Exception as e: 339 | # print(e) 340 | # print("Plugin config not found in DB, getting default") 341 | return get_plugin_info(plugin_name)["config"] 342 | else: 343 | raise HTTPException(status_code=404, detail="Plugin not found") 344 | 345 | @app.put("/plugins/set_config/{plugin_name}") 346 | def set_plugin_config(plugin_name: str, config: dict): 347 | if plugin_name in plugin_list: 348 | if plugin_name in port_mapping.keys(): 349 | memory_func = available_gpu_memory if sys.platform != "darwin" else mac_gpu_memory 350 | available_memory = memory_func() 351 | # current_model_memory = retrieve_data(f"{plugin_name}_model_memory")["memory"] 352 | # initial_memory = available_memory + current_model_memory 353 | # port = port_mapping[plugin_name] 354 | # r = client.put(f"http://127.0.0.1:{port}/set_config", json= config) 355 | job = huey_set_config(plugin_name, config, port_mapping) 356 | # after_memory = memory_func() 357 | # new_model_memory = initial_memory - after_memory 358 | # store_data(f"{plugin_name}_model_memory", {"memory": int(new_model_memory)}) 359 | return {"job_id": job.id} 360 | else: 361 | try: 362 | original_config = retrieve_data(f"plugin_config.{self.plugin_name}") 363 | except: 364 | original_config = get_plugin_info(plugin_name)["config"] 365 | original_config.update(config) 366 | store_data(f"plugin_config.{plugin_name}", original_config) 367 | else: 368 | raise HTTPException(status_code=404, detail="Plugin not found") 369 | 370 | @huey.task() 371 | def huey_set_config(plugin_name: str, config: dict, port_mapping): 372 | port = port_mapping[plugin_name] 373 | r = client.put(f"http://127.0.0.1:{port}/set_config", json= config) 374 | if r.status_code == 200: 375 | return r.json() 376 | else: 377 | raise TaskException(f"Failed to set config for {plugin_name}") 378 | 379 | @app.get("/plugins/start_plugin/{plugin_name}") 380 | async def start_plugin(plugin_name: str, port: int = None, min_port: int = 1001, max_port: int = 65534): 381 | if plugin_name not in plugin_info.keys(): 382 | get_plugin_info(plugin_name) 383 | 384 | if plugin_name in port_mapping.keys(): 385 | return {"started": True, "plugin_name": plugin_name, "port": port, "warning": "Plugin already running"} 386 | 387 | memory_func = available_gpu_memory if sys.platform != "darwin" else mac_gpu_memory 388 | 389 | # if plugin_name not in plugin_info.keys(): 390 | # get_plugin_info(plugin_name) 391 | 392 | available_memory = memory_func() 393 | 394 | if available_memory >= 0 and len(most_recent_use) > 0: 395 | if plugin_name in plugin_memory.keys(): 396 | mem_usage = retrieve_data(f"{plugin_name}_memory_mean")["memory"] 397 | while mem_usage > available_memory and len(most_recent_use) > 0: 398 | plugin_to_shutdown = most_recent_use.pop() 399 | stop_plugin(plugin_to_shutdown) 400 | time.sleep(1) 401 | available_memory = memory_func() 402 | 403 | store_data(f"{plugin_name}_available", {"memory": int(available_memory)}) 404 | 405 | plugin_states[plugin_name] = "STARTING" 406 | if port is None: 407 | port = np.random.randint(min_port,max_port) 408 | while port in list(port_mapping.values()): 409 | port = np.random.randint(min_port,max_port) 410 | elif port in list(port_mapping.values()): 411 | return {"started": False, "error": "Port already in use"} 412 | port_mapping[plugin_name] = str(port) 413 | conda_env = plugin_info[plugin_name]["plugin"]["env"] 414 | if sys.platform != "win32": 415 | if CONDA: 416 | p = subprocess.Popen(f"conda run -n {conda_env} uvicorn plugin.{plugin_name}.plugin:app --port {port}".split()) 417 | else: 418 | p = subprocess.Popen(f"envs\plugins\python -m uvicorn plugin.{plugin_name}.plugin:app --port {port}".split()) 419 | else: 420 | if CONDA: 421 | if os.getenv('CONDA_EXE'): 422 | conda_path = os.getenv('CONDA_EXE') 423 | elif sys.platform == "win32": 424 | conda_path = subprocess.check_output("echo %CONDA_EXE%", shell=True)[:-2].decode() 425 | else: 426 | conda_path = subprocess.check_output("echo $CONDA_EXE", shell=True)[:-2].decode() 427 | if not os.path.isfile(conda_path): 428 | conda_path = os.path.join(os.getenv('home'), "miniconda3", "Scripts", "conda.exe") 429 | activate_path = os.path.join(os.getenv('home'), "miniconda3", "Scripts", "activate.bat") 430 | p = subprocess.Popen(f"{activate_path} && {conda_path} run -n {conda_env} uvicorn plugin.{plugin_name}.plugin:app --port {port}", shell=True) 431 | else: 432 | p = subprocess.Popen(f"{conda_path} run -n {conda_env} uvicorn plugin.{plugin_name}.plugin:app --port {port}", shell=True) 433 | else: 434 | p = subprocess.Popen(f"envs\\plugins\\python.exe -m uvicorn plugin.{plugin_name}.plugin:app --port {port}", shell=True) 435 | pid = p.pid 436 | process_ids[plugin_name] = pid 437 | 438 | return {"started": True, "plugin_name": plugin_name, "port": port} 439 | 440 | @app.get("/plugins/stop_plugin/{plugin_name}") 441 | def stop_plugin(plugin_name: str): 442 | # need some test to ensure open port 443 | if plugin_name in process_ids.keys(): 444 | parent_pid = process_ids[plugin_name] # my example 445 | try: 446 | parent = psutil.Process(parent_pid) 447 | except NoSuchProcess: 448 | return {"status": "Failed", "Reason": f"Failed to kill {parent_pid} for plugin {plugin_name}"} 449 | try: 450 | for child in parent.children(recursive=True): # or parent.children() for recursive=False 451 | child.kill() 452 | parent.kill() 453 | except: 454 | return {"status": "Failed", "Reason": f"Failed to kill plugin {plugin_name}"} 455 | 456 | process_ids.pop(plugin_name) 457 | if plugin_name != "huey": 458 | port_mapping.pop(plugin_name) 459 | plugin_states[plugin_name] = "STOPPED" 460 | 461 | return {"status": "Success", "description": f"{plugin_name} stopped"} 462 | 463 | @app.put("/plugins/call_endpoint/{plugin_name}/{endpoint}") 464 | async def call_endpoint(plugin_name: str, endpoint: str, json_data: dict): 465 | try: 466 | print(f"Calling endpoint {endpoint} for plugin {plugin_name}, with data {json_data}") 467 | except: 468 | pass 469 | if plugin_name not in plugin_list: 470 | raise HTTPException(status_code=404, detail=f"Plugin {plugin_name} not found") 471 | if plugin_name not in port_mapping.keys(): 472 | # print(f"{plugin_name} not yet started, starting now") 473 | await start_plugin(plugin_name) 474 | if plugin_name not in plugin_endpoints.keys(): 475 | # print(f"Plugin {plugin_name} not in plugin_endpoints") 476 | get_plugin_info(plugin_name) 477 | if endpoint not in plugin_endpoints[plugin_name].keys(): 478 | raise HTTPException(status_code=404, detail=f"Endpoint {endpoint} does not exist for plugin {plugin_name}") 479 | for input in [input for input in plugin_endpoints[plugin_name][endpoint]['inputs'] if "optional=true" not in plugin_endpoints[plugin_name][endpoint]['inputs'][input]]: 480 | if input not in json_data.keys(): 481 | raise HTTPException(status_code=400, detail=f"Missing mandatory input {input} for endpoint {endpoint}") 482 | warnings = [] 483 | for input in list(json_data.keys()): 484 | if input not in plugin_endpoints[plugin_name][endpoint]['inputs'].keys(): 485 | warnings.append(f"Input '{input}' not used by endpoint '{endpoint}'") 486 | del json_data[input] 487 | memory_func = available_gpu_memory if sys.platform != "darwin" else mac_gpu_memory 488 | available_memory = memory_func() 489 | 490 | job = huey_call_endpoint(plugin_name, endpoint, json_data, port_mapping, plugin_endpoints) 491 | store_data(f"{job.id}_available", {"memory": int(available_memory), "plugin": plugin_name, "plugin_states": plugin_states, "running_jobs": get_running_jobs()["running_jobs"]}) 492 | if plugin_name in most_recent_use: 493 | most_recent_use.remove(plugin_name) 494 | most_recent_use.insert(0, plugin_name) 495 | if warnings != []: 496 | return {"job_id": job.id, "warnings": warnings} 497 | new_job(job) 498 | return {"job_id": job.id} 499 | 500 | @huey.task() 501 | def huey_call_endpoint(plugin_name: str, endpoint: str, json_data: dict, port_mapping, plugin_endpoints): 502 | result = client.get("http://127.0.0.1:8000/plugins/get_states") # Calls REST instead of directly because of missing global due to threading 503 | if result.status_code == 200: 504 | result = result.json() 505 | else: 506 | raise HTTPException(status_code=500, detail="Failed to get plugin states") 507 | counter = 0 508 | while result[plugin_name]["state"] != "RUNNING": 509 | sleep(10) 510 | result = client.get("http://127.0.0.1:8000/plugins/get_states") 511 | if result.status_code == 200: 512 | result = result.json() 513 | counter += 1 514 | if counter > 10: 515 | raise HTTPException(status_code=504, detail=f"Plugin {plugin_name} failed to start") 516 | else: 517 | port = result[plugin_name]["port"] 518 | endpoint = plugin_endpoints[plugin_name][endpoint] 519 | 520 | if "method" not in endpoint.keys(): 521 | endpoint["method"] = "GET" 522 | 523 | if endpoint['method'] == 'GET': 524 | inputs_string = "" 525 | for input in [input for input in endpoint['inputs'] if "optional=true" not in endpoint['inputs'][input]]: 526 | if input not in json_data.keys(): 527 | return {"status": "failed", "error": f"Missing required input {input}"} 528 | inputs_string += str(json_data[input]) + "/" 529 | 530 | for ct, input in enumerate([input for input in json_data.keys() if "optional=true" in endpoint['inputs'][input]]): 531 | if ct == 0: 532 | inputs_string += f"?{input}={str(json_data[input])}" 533 | else: 534 | inputs_string += f"&{input}={str(json_data[input])}" 535 | 536 | url = f"http://127.0.0.1:{port}/{endpoint['call']}/{inputs_string}" 537 | response = client.get(url, timeout=240) 538 | if response.status_code == 200: 539 | response = response.json() 540 | elif endpoint['method'] == 'PUT': 541 | url = f"http://127.0.0.1:{port}/{endpoint['call']}/" 542 | response = client.put(url, json=json_data, timeout=240) 543 | if response.status_code == 200: 544 | response = response.json() 545 | else: 546 | raise HTTPException(status_code=response.status_code, detail=response.text) 547 | else: 548 | raise HTTPException(status_code=400, detail=f"Unsupported method: {endpoint['method']}") 549 | 550 | return response 551 | 552 | @app.get("/plugin/status/") 553 | def get_all_plugin_status(): 554 | return {"plugin_states": plugin_states} 555 | 556 | @app.get("/plugin/status/{plugin_name}") 557 | def get_plugin_status(plugin_name: str): 558 | status = plugin_states.get(plugin_name, "PLUGIN NOT FOUND") 559 | return {"plugin_name": plugin_name, "status": status} 560 | 561 | @app.post("/plugin_callback/{plugin_name}/{status}") 562 | def plugin_callback(plugin_name: str, status: str): 563 | running = status == "True" 564 | current_state = plugin_states.get(plugin_name) 565 | if sys.platform != "darwin": 566 | memory_func = available_gpu_memory 567 | else: 568 | memory_func = mac_gpu_memory 569 | try: 570 | print(f"Callback received for plugin: {plugin_name}. Current state: {current_state}") 571 | except: 572 | pass 573 | if running: 574 | plugin_states[plugin_name] = "RUNNING" 575 | # print(f"{plugin_name} is now in RUNNING state") 576 | for plugin in plugin_states.keys(): 577 | if plugin_states[plugin] == "STARTING" or len(running_jobs) > 0: 578 | return {"status": "success", "message": f"{plugin_name} is now in RUNNING state"} 579 | initial_memory = retrieve_data(f"{plugin_name}_available")["memory"] 580 | memory_left = memory_func() 581 | model_memory = initial_memory - memory_left 582 | 583 | # store_data(f"{plugin_name}_model_memory", {"memory": int(model_memory)}) 584 | # model_memory = store_data(f"{plugin_name}_model_memory")["memory"] 585 | 586 | return {"status": "success", "message": f"{plugin_name} is now in RUNNING state"} 587 | else: 588 | # print(f"{plugin_name} failed to start") 589 | plugin_states.pop(plugin_name) 590 | return {"status": "error", "message": f"{plugin_name} failed to start because {status}"} 591 | 592 | @app.post("/plugin_install_callback/{plugin_name}/{progress}/{stage}") 593 | async def plugin_install_callback(plugin_name: str, progress: float, stage: str): 594 | # Handle installation progress update here 595 | # print(f"Installation progress for {plugin_name}: {progress}% complete. Current stage: {stage}") 596 | return {"status": "success", "message": f"Received installation progress for {plugin_name}"} 597 | 598 | @app.post("/plugin_uninstall_callback/{plugin_name}/{progress}/{stage}") 599 | async def plugin_uninstall_callback(plugin_name: str, progress: float, stage: str): 600 | # Handle uninstallation progress update here 601 | # print(f"Unistallation progress for {plugin_name}: {progress}% complete. Current stage: {stage}") 602 | return {"status": "success", "message": f"Received uninstallation progress for {plugin_name}"} 603 | 604 | @app.get("/plugins/get_jobs") 605 | def get_running_jobs(): 606 | for job in running_jobs: 607 | try: 608 | job = jobs[job] 609 | if job() is not None: 610 | move_job(job.id) 611 | except Exception as e: 612 | if isinstance(e, TaskException): 613 | move_job(job.id) 614 | return {"running_jobs": running_jobs, "finished_jobs": finished_jobs} 615 | 616 | @app.get("/backend/shutdown") 617 | def shutdown(): 618 | for plugin_name in list(process_ids.keys()): 619 | stop_plugin(plugin_name) 620 | 621 | if os.path.exists(os.path.join(storage_folder, "huey")): 622 | shutil.rmtree(os.path.join(storage_folder, "huey")) 623 | if os.path.exists(os.path.join(storage_folder, "huey_storage")): 624 | shutil.rmtree(os.path.join(storage_folder, "huey_storage")) 625 | if os.path.exists(os.path.join(storage_folder, "huey.db")): 626 | try: 627 | os.remove(os.path.join(storage_folder, "huey.db")) 628 | except PermissionError: 629 | # print("Failed to remove huey.db") 630 | pass 631 | 632 | stop_plugin("main") 633 | 634 | @app.on_event("shutdown") 635 | async def shutdown_event(): 636 | for plugin_name in list(process_ids.keys()): 637 | stop_plugin(plugin_name) 638 | 639 | if os.path.exists(os.path.join(storage_folder, "huey")): 640 | shutil.rmtree(os.path.join(storage_folder, "huey")) 641 | if os.path.exists(os.path.join(storage_folder, "huey_storage")): 642 | shutil.rmtree(os.path.join(storage_folder, "huey_storage")) 643 | if os.path.exists(os.path.join(storage_folder, "huey.db")): 644 | try: 645 | os.remove(os.path.join(storage_folder, "huey.db")) 646 | except PermissionError: 647 | # print("Failed to remove huey.db") 648 | pass 649 | 650 | @app.put("/job") 651 | def add_job(job: Job): 652 | print("Received Job Payload:", job.dict()) # Print the received payload 653 | new_job(job) 654 | return {"message": f"Job {job.message_id} added"} 655 | 656 | @huey.post_execute() 657 | def record_memory(task, task_value, exc): 658 | if task_value is not None: 659 | try: 660 | task_data = retrieve_data(f"{task.id}_available") 661 | except: 662 | return 663 | 664 | plugin_states = task_data["plugin_states"] 665 | running_jobs = task_data["running_jobs"] 666 | for plugin in plugin_states.keys(): 667 | if plugin_states[plugin] == "STARTING" or len(running_jobs) > 1: 668 | return task_value 669 | initial_memory = task_data["memory"] 670 | plugin_name = task_data["plugin"] 671 | # plugin_model_memory = retrieve_data(f"{plugin_name}_model_memory")["memory"] 672 | 673 | memory_func = available_gpu_memory if sys.platform != "darwin" else mac_gpu_memory 674 | memory_left = memory_func() 675 | # inference_memory = int(initial_memory - memory_left + plugin_model_memory) 676 | inference_memory = int(initial_memory - memory_left) 677 | 678 | mem_list = retrieve_data(f"{plugin_name}_memory")["memory"] 679 | mem_list.append(inference_memory) 680 | store_data(f"{plugin_name}_memory", {"memory": mem_list}) 681 | store_data(f"{plugin_name}_memory_mean", {"memory": int(np.mean(mem_list))}) 682 | store_data(f"{plugin_name}_memory_max", {"memory": int(np.max(mem_list))}) 683 | store_data(f"{plugin_name}_memory_min", {"memory": int(np.min(mem_list))}) 684 | delete_data(f"{task.id}_available") 685 | return task_value 686 | return task_value 687 | 688 | def move_job(job_id): 689 | if job_id in running_jobs: 690 | 691 | running_jobs.remove(job_id) 692 | finished_jobs.append(job_id) 693 | 694 | @app.get("/job/{job_id}") 695 | def get_job(job_id: str): 696 | try: 697 | job = jobs[job_id] 698 | if job() is None: 699 | return {"status": "Job in progress"} 700 | except Exception as e: 701 | if isinstance(e, KeyError): 702 | return {"status": "Job not found"} 703 | # print("moving job") 704 | move_job(job_id) 705 | # print("found job") 706 | try: 707 | result = job() 708 | except TaskException as e: 709 | return {"status": "Job failed", "detail": str(e)} 710 | return result 711 | 712 | @app.get("/image/get/{img_id}") 713 | async def get_img(img_id: str): 714 | image_bytes = fetch_image(img_id) 715 | return Response(content=image_bytes, media_type="image/png") 716 | 717 | @app.post("/image/upload") 718 | async def upload_img(file: UploadFile): 719 | # serialized_image = await serialize_image(file) 720 | image_id = await store_image(file) 721 | return {"status": "Success", "image_id": image_id} 722 | 723 | @app.post("/image/upload_multiple") 724 | async def upload_images(files: list[UploadFile]): 725 | image_id = await store_multiple_images(files) 726 | return {"status": "Success", "image_id": image_id} 727 | 728 | async def store_multiple_images(data): 729 | img_data = [] 730 | for image in data: 731 | image_bytes = await image.read() 732 | img_data.append(Image.open(BytesIO(image_bytes))) 733 | shape = np.array(img_data).shape 734 | img_data = np.array(img_data).tobytes() 735 | img_id = str(uuid.uuid4()) 736 | shape_id = img_id + "_shape" 737 | storage.put_data(img_id,img_data) 738 | storage.put_data(shape_id, np.array(shape).tobytes()) 739 | return img_id 740 | 741 | @app.put("/data/store/{key}") 742 | def store_data(key: str, item: dict): 743 | conn = sqlite3.connect(os.path.join(storage_folder, 'data_storage.db')) 744 | cursor = conn.cursor() 745 | value = json.dumps(dict(item)) 746 | cursor.execute("REPLACE INTO key_value_store (key, value) VALUES (?, ?)", (key, value)) 747 | conn.commit() 748 | conn.close() 749 | return {"message": "Data stored successfully"} 750 | 751 | @app.get("/data/retrieve/{key}") 752 | def retrieve_data(key: str): 753 | conn = sqlite3.connect(os.path.join(storage_folder, 'data_storage.db')) 754 | cursor = conn.cursor() 755 | cursor.execute("SELECT value FROM key_value_store WHERE key = ?", (key,)) 756 | row = cursor.fetchone() 757 | conn.close() 758 | if row: 759 | data = json.loads(row[0]) 760 | return data 761 | raise HTTPException(status_code=404, detail="Key not found") 762 | 763 | @app.delete("/data/delete/{key}") 764 | def delete_data(key: str): 765 | conn = sqlite3.connect(os.path.join(storage_folder, 'data_storage.db')) 766 | cursor = conn.cursor() 767 | cursor.execute("DELETE FROM key_value_store WHERE key = ?", (key,)) 768 | conn.commit() 769 | conn.close() 770 | return {"message": "Data deleted successfully"} -------------------------------------------------------------------------------- /gui.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QWidget, QPushButton, QLineEdit, QTextEdit, QComboBox, QSlider, QSizePolicy, QHBoxLayout, QVBoxLayout, QCheckBox, QDialog, QScrollArea, QDialogButtonBox, QLabel, QFileDialog, QTableWidget, QHeaderView, QTableWidgetItem, QApplication, QProgressBar, QComboBox, QHBoxLayout, QListWidget, QHeaderView, QTableWidget, QVBoxLayout, QTableWidgetItem, QDialog, QScrollArea, QDialogButtonBox, QSlider, QSizePolicy, QCheckBox, QLabel, QLineEdit, QFileDialog, QMessageBox 2 | from PySide6.QtCore import Qt, Signal, QObject, QThread, QTimer, QEventLoop 3 | from PySide6.QtGui import QIcon, QPixmap 4 | import os 5 | fastapi_launcher_path = os.path.join(os.path.dirname(__file__), "plugin") 6 | import sys 7 | import requests 8 | import subprocess 9 | import webbrowser 10 | import time 11 | 12 | client = requests.Session() 13 | 14 | 15 | 16 | class setWorker(QObject): 17 | name = "" 18 | output_config = {} 19 | finished = Signal() 20 | # progress = Signal(int) 21 | def run(self): 22 | """Long-running task.""" 23 | r = client.put(f"http://127.0.0.1:8000/plugins/set_config/{self.name}", json = self.output_config) 24 | 25 | self.finished.emit() 26 | 27 | 28 | class ConfigGUI(QWidget): 29 | def __init__(self, plugin_name): 30 | super().__init__() 31 | self.name = plugin_name 32 | self.title = f"{plugin_name} Configuration" 33 | self.left = 100 34 | self.top = 100 35 | self.width = 800 36 | self.height = 300 37 | self.setWindowTitle(self.title) 38 | self.setGeometry(self.left, self.top, self.width, self.height) 39 | 40 | r = client.get(f"http://127.0.0.1:8000/plugins/get_config/{plugin_name}") 41 | config = r.json() 42 | self.output_config = config 43 | self.layout = QVBoxLayout() 44 | self.widget_dict = {} 45 | 46 | 47 | self.guiFromConfig(config) 48 | 49 | 50 | self.submit_button = QPushButton("Submit") 51 | self.submit_button.clicked.connect(self.start) 52 | self.layout.addWidget(self.submit_button) 53 | self.setLayout(self.layout) 54 | # self.setFixedSize(self.layout.sizeHint()) 55 | 56 | 57 | def guiFromConfig(self, config): 58 | for key in config: 59 | value = config[key] 60 | label = QLabel(key) 61 | self.layout.addWidget(label) 62 | 63 | if isinstance(value, list): 64 | dropdown = QComboBox() 65 | for item in value: 66 | dropdown.addItem(item) 67 | dropdown.setEditable(True) 68 | dropdown.currentTextChanged.connect(lambda text, key = key: self.editConfig(key, text)) 69 | self.layout.addWidget(dropdown) 70 | elif isinstance(value, bool): 71 | checkbox = QCheckBox(f"{key}") 72 | checkbox.stateChanged.connect(lambda state, key = key: self.setBool(key, state)) 73 | self.layout.addWidget(checkbox) 74 | elif isinstance(value, int): 75 | h_layout = QHBoxLayout() 76 | number_label = QLabel(f"{value}") 77 | 78 | slider = QSlider(Qt.Orientation.Horizontal) 79 | slider.setMinimum(1) 80 | slider.setMaximum(10000) 81 | slider.setValue(value) 82 | slider.valueChanged.connect(lambda number, key = key: self.editConfig(key, number)) 83 | slider.valueChanged.connect(lambda number, label= number_label: self.changeValue(label, number)) 84 | policy = slider.sizePolicy() 85 | policy.setHorizontalPolicy(QSizePolicy.Policy.Expanding) 86 | slider.setSizePolicy(policy) 87 | h_layout.addWidget(slider) 88 | h_layout.addWidget(number_label) 89 | self.layout.addLayout(h_layout) 90 | elif value == "Image": 91 | h_layout = QHBoxLayout() 92 | file_button = QPushButton("Choose File") 93 | image_label = QLabel("No File Chosen") 94 | 95 | file_button.clicked.connect(lambda: self.openFileNameDialog(image_label)) 96 | h_layout.addWidget(image_label) 97 | h_layout.addWidget(file_button) 98 | self.layout.addLayout(h_layout) 99 | elif value == "Point": 100 | h_layout = QHBoxLayout() 101 | x_label = QLabel("X") 102 | x_box = QLineEdit("0") 103 | y_label = QLabel("Y") 104 | y_box = QLineEdit("1") 105 | x_box.textChanged.connect(lambda text, key = key: self.editConfigNumberList(key, [text, y_box.text()], float)) 106 | y_box.textChanged.connect(lambda text, key = key: self.editConfigNumberList(key, [x_box.text(), text], float)) 107 | h_layout.addWidget(x_label) 108 | h_layout.addWidget(x_box) 109 | h_layout.addWidget(y_label) 110 | h_layout.addWidget(y_box) 111 | self.layout.addLayout(h_layout) 112 | elif value == "Box": 113 | h_layout = QHBoxLayout() 114 | x_label = QLabel("X") 115 | x_box = QLineEdit("0") 116 | y_label = QLabel("Y") 117 | y_box = QLineEdit("1") 118 | w_label = QLabel("X") 119 | w_box = QLineEdit("0") 120 | h_label = QLabel("Y") 121 | h_box = QLineEdit("1") 122 | x_box.textChanged.connect(lambda text, key = key: self.editConfigNumberList(key, [text, y_box.text(), w_box.text(), h_box.text()], float)) 123 | y_box.textChanged.connect(lambda text, key = key: self.editConfigNumberList(key, [x_box.text(), text, w_box.text(), h_box.text()], float)) 124 | w_box.textChanged.connect(lambda text, key = key: self.editConfigNumberList(key, [x_box.text(), y_box.text(), text, h_box.text()], float)) 125 | h_box.textChanged.connect(lambda text, key = key: self.editConfigNumberList(key, [x_box.text(), y_box.text(), w_box.text(), text], float)) 126 | h_layout.addWidget(x_label) 127 | h_layout.addWidget(x_box) 128 | h_layout.addWidget(y_label) 129 | h_layout.addWidget(y_box) 130 | h_layout.addWidget(w_label) 131 | h_layout.addWidget(w_box) 132 | h_layout.addWidget(h_label) 133 | h_layout.addWidget(h_box) 134 | self.layout.addLayout(h_layout) 135 | elif isinstance(value, str): 136 | # self.widget_dict[key] = QLineEdit(f"{value}") 137 | text_box = QLineEdit(f"{value}") 138 | # text_box.textChanged.connect(lambda key = key, text=text_box.text(): self.editConfig(key, text)) 139 | text_box.textChanged.connect(lambda text, key = key: self.editConfig(key, text)) 140 | # self.widget_dict[key] = text_box 141 | self.layout.addWidget(text_box) 142 | 143 | 144 | 145 | def openFileNameDialog(self, image_label): 146 | fname = QFileDialog.getOpenFileName(self, 'Open file', 147 | 'c:\\',"Image files (*.jpg *.png)") 148 | # image_label.setPixmap(QPixmap(fname)) 149 | image_label.setText(fname[0]) 150 | self.editConfig("image", fname[0]) 151 | 152 | def editConfigNumberList(self, key, num_list, func): 153 | list_to_send = [] 154 | try: 155 | for num in num_list: 156 | num_form = func(num) 157 | if num_form < 0: 158 | num_form = 0.0 159 | elif num_form > 1: 160 | num_form = 1.0 161 | list_to_send.append(num_form) 162 | self.output_config[key] = list_to_send 163 | print(self.output_config) 164 | except: 165 | print("One or more inputs invalid") 166 | 167 | 168 | def changeValue(self, label, value): 169 | label.setText(f"{value}") 170 | def editConfig(self, key, text): 171 | self.output_config[key] = text 172 | def setBool(self, key, state): 173 | if state == 2: 174 | self.output_config[key] = True 175 | else: 176 | self.output_config[key] = False 177 | 178 | def thread_process(self): 179 | 180 | self.thread = QThread() 181 | 182 | self.worker = setWorker() 183 | self.worker.name = self.name 184 | self.worker.output_config = self.output_config 185 | 186 | self.worker.moveToThread(self.thread) 187 | self.thread.started.connect(self.worker.run) 188 | self.worker.finished.connect(self.thread.quit) 189 | self.worker.finished.connect(self.worker.deleteLater) 190 | self.thread.finished.connect(self.thread.deleteLater) 191 | self.thread.start() 192 | self.thread.finished.connect(self.renable) 193 | 194 | def renable(self): 195 | self.submit_button.setEnabled(True) 196 | self.submit_button.setText("Submit") 197 | 198 | def start(self): 199 | self.submit_button.setEnabled(False) 200 | self.submit_button.setText("Submitting...") 201 | self.thread_process() 202 | 203 | class Worker(QObject): 204 | popen_string = " " 205 | plugin_name = "" 206 | finished = Signal() 207 | plugin_dict = {} 208 | cur_string = Signal(str) 209 | failed = Signal() 210 | status = True 211 | _isRunning = True 212 | # progress = Signal(int) 213 | def run(self): 214 | "Actually installs the files" 215 | if self._isRunning: 216 | r = client.get(f"http://127.0.0.1:8000/plugin_manager/install/{self.plugin_name}") 217 | if r.status_code == 200: 218 | result = r.json() 219 | else: 220 | self.failed.emit() 221 | return 222 | installed = False 223 | while not installed: 224 | time.sleep(5) 225 | r = client.get(f"http://127.0.0.1:8000/plugins/get_states") 226 | if r.status_code == 200: 227 | result = r.json() 228 | if self.plugin_name in result: 229 | if result[self.plugin_name]["state"] == "STOPPED": 230 | installed = True 231 | 232 | self.status = False 233 | self.finished.emit() 234 | 235 | def stop(self): 236 | self._isRunning = False 237 | 238 | class UninstallWorker(QObject): 239 | plugin_name = "" 240 | finished = Signal() 241 | def run(self): 242 | 243 | # Potential on_uninstall 244 | # r = client.get(f"http://127.0.0.1:8000/plugins/start_plugin/{self.plugin_name}") 245 | # result = client.get("http://127.0.0.1:8000/plugins/get_states").json() 246 | # while result[self.plugin_name]["state"] != "RUNNING": 247 | # time.sleep(10) 248 | # result = client.get("http://127.0.0.1:8000/plugins/get_states").json() 249 | # r = client.get(f"http://127.0.0.1:8000/plugins/uninstall/{self.plugin_name}") 250 | # r = client.get(f"http://127.0.0.1:8000/plugins/stop_plugin/{self.plugin_name}") 251 | 252 | # Removes files after uninstall process 253 | r = client.get(f"http://127.0.0.1:8000/plugin_manager/uninstall/{self.plugin_name}") 254 | 255 | self.finished.emit() 256 | 257 | 258 | class PluginManagerGUI(QWidget): 259 | def __init__(self): 260 | super().__init__() 261 | 262 | self.title = "Plugin Manager" 263 | self.left = 0 264 | self.top = 0 265 | self.width = 1300 266 | self.height = 300 267 | self.setWindowTitle(self.title) 268 | self.setGeometry(100, 100, self.width, self.height) 269 | # self.setStyleSheet( "color: white; border-color: #7b3bff") 270 | 271 | 272 | self.plugin_dict = requests.get("http://127.0.0.1:8000/plugin_manager/get_plugin_info").json() 273 | 274 | self.createTable() 275 | 276 | self.scrollArea = QScrollArea() 277 | self.scrollArea.setWidget(self.tableWidget) 278 | # compute the correct minimum width 279 | width = (self.tableWidget.sizeHint().width() + 280 | self.scrollArea.verticalScrollBar().sizeHint().width() + 281 | self.scrollArea.frameWidth() * 2) 282 | self.scrollArea.setMinimumWidth(width) 283 | self.aewarning = QLabel("Restart After Effects for any changes to take effect.") 284 | self.aewarning.setStyleSheet("font-size: 20px; font-weight: bold;") 285 | 286 | self.layout = QVBoxLayout() 287 | self.layout.addWidget(self.tableWidget) 288 | self.layout.addWidget(self.aewarning) 289 | self.setLayout(self.layout) 290 | self.threads = [QThread() for i in range(len(self.plugin_dict.keys()))] 291 | self.workers = [Worker() for i in range(len(self.plugin_dict.keys()))] 292 | # self.install_threads = [QThread() for i in range(len(self.plugin_dict.keys()))] 293 | # self.install_workers = [Worker() for i in range(len(self.plugin_dict.keys()))] 294 | 295 | 296 | 297 | 298 | # self.setFixedSize(self.layout.sizeHint()) 299 | 300 | 301 | def createTable(self): 302 | self.tableWidget = QTableWidget() 303 | row_count = len(self.plugin_dict.keys()) 304 | column_count = 5 305 | 306 | self.tableWidget.setRowCount(row_count) 307 | self.tableWidget.setColumnCount(column_count) 308 | self.button_dict = {} 309 | 310 | for row in range(row_count): 311 | plugin_name = list(self.plugin_dict.keys())[row] 312 | for col in range(column_count): 313 | if col == 0: 314 | plugin_name_item = QTableWidgetItem(plugin_name) 315 | plugin_name_item.setFlags(Qt.ItemIsEnabled) 316 | self.tableWidget.setItem(row, col, plugin_name_item) 317 | elif col == 1: 318 | description_item = QTableWidgetItem(self.plugin_dict[plugin_name]["Description"]) 319 | description_item.setFlags(Qt.ItemIsEnabled) 320 | self.tableWidget.setItem(row, col, description_item) 321 | elif col == 2: 322 | if "Version" not in self.plugin_dict[plugin_name].keys(): 323 | item = QTableWidgetItem("0.0.0") 324 | else: 325 | item = QTableWidgetItem(self.plugin_dict[plugin_name]["Version"]) 326 | item.setFlags(Qt.ItemIsEnabled) 327 | self.tableWidget.setItem(row, col, item) 328 | item.setTextAlignment(Qt.AlignmentFlag.AlignHCenter) 329 | elif col == 3: 330 | if plugin_name in os.listdir(fastapi_launcher_path): 331 | self.Installed(plugin_name) 332 | else: 333 | self.install_button_creation(plugin_name) 334 | elif col == 4: 335 | if plugin_name in os.listdir(fastapi_launcher_path): 336 | if "url" in self.plugin_dict[plugin_name].keys(): 337 | 338 | button = QPushButton(f"Manage") 339 | button.clicked.connect(lambda _, name = plugin_name: self.uninstall_plugin(name)) 340 | self.button_dict[plugin_name] = button 341 | self.tableWidget.setCellWidget(row, col, button) 342 | else: 343 | self.manage(plugin_name) 344 | 345 | 346 | 347 | self.tableWidget.setShowGrid(False) 348 | self.tableWidget.setHorizontalHeaderLabels(['Name', 'Description', 'Version', 'Install', "Manage"]) 349 | self.tableWidget.verticalHeader().setVisible(False) 350 | self.tableWidget.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) 351 | self.tableWidget.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) 352 | self.tableWidget.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) 353 | self.tableWidget.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch) 354 | self.tableWidget.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeMode.Stretch) 355 | 356 | def install_button_creation(self, plugin_name): 357 | 358 | row = list(self.plugin_dict.keys()).index(plugin_name) 359 | if "url" not in self.plugin_dict[plugin_name].keys(): 360 | button = QPushButton(f"Subscribe") 361 | button.clicked.connect(lambda: webbrowser.open("https://deepmake.com")) 362 | else: 363 | button = QPushButton(f"Install") 364 | button.clicked.connect(lambda _, name = plugin_name: self.install_plugin(name)) 365 | self.button_dict[plugin_name] = button 366 | self.tableWidget.setItem(row, 3, QTableWidgetItem(" ")) 367 | self.tableWidget.setCellWidget(row, 3, button) 368 | 369 | def install_plugin(self, plugin_name): 370 | if plugin_name in os.listdir(fastapi_launcher_path): 371 | print("Plugin already installed") 372 | return 373 | 374 | row_number = list(self.plugin_dict.keys()).index(plugin_name) 375 | # installing_button = QPushButton(f"Installing") 376 | self.tableWidget.removeCellWidget(row_number, 3) 377 | 378 | installing_item = QTableWidgetItem("Installing...") 379 | installing_item.setFlags(Qt.ItemIsEnabled) 380 | self.tableWidget.setItem(row_number, 3, installing_item) 381 | installing_item.setTextAlignment(Qt.AlignmentFlag.AlignHCenter) 382 | folder_path = os.path.join(os.path.dirname(__file__), "plugin", plugin_name) 383 | # if row_number == 0: 384 | # thread = self.thread0 385 | # elif row_number == 1: 386 | # thread = self.thread1 387 | # elif row_number == 2: 388 | # thread = self.thread2 389 | # elif row_number == 3: 390 | # thread = self.thread3 391 | self.deactivate_install_button(plugin_name) 392 | 393 | thread = "" 394 | self.thread_process(f"conda env create -f {folder_path}/environment.yml", row_number, thread) 395 | 396 | def deactivate_install_button(self, plugin_name): 397 | row = list(self.plugin_dict.keys()).index(plugin_name) 398 | 399 | for name in self.button_dict.keys(): 400 | if name != plugin_name: 401 | self.button_dict[name].setDisabled(True) 402 | self.button_dict[name].setStyleSheet("color: grey") 403 | 404 | 405 | 406 | def uninstall_plugin(self, plugin_name): 407 | if plugin_name not in os.listdir(fastapi_launcher_path): 408 | print("Plugin not installed") 409 | return 410 | dlg = CustomDialog(plugin_name, self.plugin_dict[plugin_name]) 411 | if dlg.exec(): 412 | print("Uninstalling", plugin_name) 413 | self.thread_uninstall(plugin_name) 414 | # self.button_dict.pop(plugin_name) 415 | # self.tableWidget.removeCellWidget(list(self.plugin_dict.keys()).index(plugin_name), 2) 416 | self.install_button_creation(plugin_name) 417 | self.manage(plugin_name) 418 | def changeVersion(self, text): 419 | self.tableWidget.setCellWidget(1, 2, text) 420 | 421 | def thread_uninstall(self, plugin_name): 422 | self.uninstall_worker = UninstallWorker() 423 | self.uninstall_thread = QThread() 424 | self.uninstall_worker.plugin_name = plugin_name 425 | 426 | self.uninstall_worker.moveToThread(self.uninstall_thread) 427 | self.uninstall_thread.started.connect(self.uninstall_worker.run) 428 | self.uninstall_worker.finished.connect(self.uninstall_thread.quit) 429 | self.uninstall_worker.finished.connect(self.uninstall_worker.deleteLater) 430 | self.uninstall_thread.finished.connect(self.uninstall_thread.deleteLater) 431 | 432 | self.uninstall_thread.start() 433 | self.uninstall_thread.finished.connect(lambda: self.button_dict.pop(plugin_name)) 434 | self.uninstall_thread.finished.connect(lambda: self.tableWidget.removeCellWidget(list(self.plugin_dict.keys()).index(plugin_name), 4)) 435 | self.uninstall_thread.finished.connect(lambda: self.install_button_creation(plugin_name)) 436 | self.uninstall_thread.finished.connect(lambda: self.manage(plugin_name)) 437 | 438 | 439 | def manage(self, plugin_name): 440 | row = list(self.plugin_dict.keys()).index(plugin_name) 441 | item = QTableWidgetItem("Install First!") 442 | item.setFlags(Qt.ItemIsEnabled) 443 | item.setTextAlignment(Qt.AlignmentFlag.AlignHCenter) 444 | self.tableWidget.removeCellWidget(row, 4) 445 | self.tableWidget.setItem(row, 4, item) 446 | 447 | 448 | def uninstall_button_creation(self, plugin_name): 449 | row = list(self.plugin_dict.keys()).index(plugin_name) 450 | button = QPushButton(f"Manage") 451 | button.clicked.connect(lambda _, name = plugin_name: self.uninstall_plugin(name)) 452 | self.button_dict[plugin_name] = button 453 | self.tableWidget.setItem(row, 4, QTableWidgetItem(" ")) 454 | self.tableWidget.setCellWidget(row, 4, button) 455 | print("Finish env", plugin_name) 456 | 457 | def closeEvent(self, event): 458 | for i in range(len(self.workers)): 459 | worker = self.workers[i] 460 | thread = self.threads[i] 461 | worker.finished.connect(worker.deleteLater) 462 | thread.finished.connect(thread.deleteLater) 463 | 464 | worker.finished.emit() 465 | worker.stop() 466 | # thread.requestInterruption() 467 | thread.quit() 468 | thread.wait() 469 | 470 | 471 | # for thread in self.threads: 472 | # thread.quit() 473 | # thread.wait 474 | event.accept() 475 | 476 | def thread_process(self, popen_string, row_number, thread): 477 | 478 | plugin_name = list(self.plugin_dict.keys())[row_number] 479 | # while not self.thread.isFinished(): 480 | # time.sleep(3) 481 | # wait_label = QTableWidgetItem(f"Waiting...") 482 | # self.tableWidget.removeCellWidget(row_number, 3) 483 | # self.tableWidget.setItem(row_number, 3, wait_label) 484 | # print("Waiting for thread to finish") 485 | # self.thread = QThread() 486 | worker = self.workers[row_number] 487 | thread = self.threads[row_number] 488 | # installing_worker = self.install_workers[row_number] 489 | # installing_thread = self.install_threads[row_number] 490 | 491 | worker.popen_string = popen_string 492 | worker.plugin_name = plugin_name 493 | worker.plugin_dict = self.plugin_dict 494 | 495 | 496 | worker.moveToThread(thread) 497 | thread.started.connect(worker.run) 498 | worker.finished.connect(thread.quit) 499 | # worker.finished.connect(worker.deleteLater) 500 | # thread.finished.connect(thread.deleteLater) 501 | worker.failed.connect(thread.quit) 502 | # worker.failed.connect(worker.deleteLater) 503 | 504 | worker.failed.connect(lambda: self.install_button_creation(plugin_name)) 505 | worker.failed.connect(self.activate_install_buttons) 506 | 507 | # thread.finished.connect(lambda: self.uninstall_button_creation(plugin_name)) 508 | # thread.finished.connect(lambda: self.Installed(plugin_name)) 509 | # thread.finished.connect(self.activate_install_buttons) 510 | 511 | worker.finished.connect(lambda: self.Installed(plugin_name)) 512 | worker.finished.connect(lambda: self.uninstall_button_creation(plugin_name)) 513 | worker.finished.connect(self.activate_install_buttons) 514 | 515 | # installing_worker.moveToThread(installing_thread) 516 | # installing_thread.started.connect(installing_worker.changeText) 517 | # installing_worker.cur_string.connect(lambda text: self.tableWidget.item(row_number, 3).setText(text)) 518 | # worker.finished.connect(installing_thread.quit) 519 | # worker.finished.connect(installing_worker.finished) 520 | # installing_worker.finished.connect(installing_worker.deleteLater) 521 | # installing_thread.finished.connect(installing_thread.deleteLater) 522 | thread.start() 523 | # installing_thread.start() 524 | 525 | 526 | def activate_install_buttons(self): 527 | for name in self.button_dict.keys(): 528 | self.button_dict[name].setEnabled(True) 529 | self.button_dict[name].setStyleSheet("color: black") 530 | 531 | def Installed(self, plugin_name): 532 | row = list(self.plugin_dict.keys()).index(plugin_name) 533 | install_label = QTableWidgetItem("Installed") 534 | install_label.setFlags(Qt.ItemIsEnabled) 535 | install_label.setTextAlignment(Qt.AlignmentFlag.AlignHCenter) 536 | self.tableWidget.setItem(row, 3, install_label) 537 | 538 | class CustomDialog(QDialog): 539 | def __init__(self, plugin_name, plugin_info): 540 | super().__init__() 541 | self.name = plugin_name 542 | self.setWindowTitle("Manage") 543 | self.setGeometry(0, 0, 500, 200) 544 | self.plugin_info = plugin_info 545 | self.install_url = self.plugin_info["url"] 546 | 547 | self.buttonBox = QDialogButtonBox() 548 | self.buttonBox.addButton("Uninstall", QDialogButtonBox.ButtonRole.AcceptRole) 549 | # self.buttonBox.addButton("Update to Latest", QDialogButtonBox.ButtonRole.RejectRole) 550 | self.buttonBox.accepted.connect(self.accept) 551 | # self.buttonBox.centerButtons() 552 | # uninstall_button = QPushButton() 553 | # uninstall_button.addButton("Uninstall", QDialogButtonBox.ButtonRole.AcceptRole) 554 | self.layout = QVBoxLayout() 555 | self.createTable() 556 | self.update_button = QPushButton("Update to Latest") 557 | self.update_button.clicked.connect(lambda: self.update_plugin(self.name)) 558 | # self.test_button = QPushButton("Test") 559 | self.tableWidget.setCellWidget(1,0, self.buttonBox) 560 | self.tableWidget.setCellWidget(1, 2, self.update_button) 561 | # self.tableWidget.setCellWidget(1, 1,self.test_button) 562 | # self.test_button.clicked.connect(self.test_plugin) 563 | # message = QLabel("Do you want to uninstall?") 564 | # self.layout.addWidget(message) 565 | self.layout.addWidget(self.tableWidget) 566 | # self.layout.addWidget(self.buttonBox) 567 | self.setLayout(self.layout) 568 | 569 | def update_plugin(self, plugin_name): 570 | if len(self.tag_list) == 0: 571 | print("No versioning") 572 | return 573 | version = self.update_button.text().split()[-1] 574 | if version == "Latest": 575 | version = self.tag_list[0] 576 | r = client.get(f"http://127.0.0.1:8000/plugin_manager/update/{plugin_name}/{version}") 577 | # current_tag = self.getVersion() 578 | version_item = QTableWidgetItem(f"Current Version: {version}") 579 | version_item.setFlags(Qt.ItemIsEnabled) 580 | self.tableWidget.setItem(0, 0, version_item) 581 | print("Updated", plugin_name, "to version", version) 582 | 583 | def version_management(self): 584 | tag_list = [] 585 | if ".git" in self.install_url: 586 | origin_folder = os.path.dirname(__file__) 587 | os.chdir(os.path.join(origin_folder, "plugin", self.name)) 588 | tags = subprocess.check_output("git tag".split()).decode("utf-8") 589 | os.chdir(origin_folder) 590 | for label in tags.split("\n")[:-1]: 591 | tag_list.append(label) 592 | else: 593 | tag_list.append(self.install_url.split("/")[-1].split("-")[1].split(".")[0]) 594 | # tag_list.append("No Versioning") 595 | return tag_list 596 | 597 | def createTable(self): 598 | self.tableWidget = QTableWidget() 599 | row_count = 2 600 | column_count = 3 601 | 602 | self.tableWidget.setRowCount(row_count) 603 | self.tableWidget.setColumnCount(column_count) 604 | self.button_dict = {} 605 | current_tag = self.getVersion() 606 | cur_ver_item = QTableWidgetItem(f"Current Version: {current_tag}") 607 | avail_item = QTableWidgetItem("Available Versions") 608 | cur_ver_item.setFlags(Qt.ItemIsEnabled) 609 | avail_item.setFlags(Qt.ItemIsEnabled) 610 | self.tableWidget.setItem(0, 0, cur_ver_item) 611 | self.tableWidget.setItem(0, 1, avail_item) 612 | dropdown = QComboBox() 613 | self.tag_list = self.version_management() 614 | self.tag_list.reverse() 615 | dropdown.addItems(self.tag_list) 616 | dropdown.currentTextChanged.connect(self.changeVersion) 617 | 618 | self.tableWidget.setCellWidget(0, 2, dropdown) 619 | 620 | self.tableWidget.setShowGrid(False) 621 | self.tableWidget.horizontalHeader().setVisible(False) 622 | self.tableWidget.verticalHeader().setVisible(False) 623 | # self.tableWidget.setHorizontalHeaderLabels(['Name', 'Description', 'Version', 'Install']) 624 | 625 | self.tableWidget.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) 626 | self.tableWidget.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) 627 | self.tableWidget.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) 628 | 629 | def changeVersion(self, text): 630 | self.update_button.setText(f"Update to {text}") 631 | 632 | def getVersion(self): 633 | if ".git" in self.install_url: 634 | origin_folder = os.path.join(fastapi_launcher_path, self.name) 635 | os.chdir(origin_folder) 636 | try: 637 | tag = subprocess.check_output("git describe --tags".split()).decode("utf-8").split("\n")[0] 638 | except: 639 | tag = "0.0.0" 640 | os.chdir(os.path.dirname(__file__)) 641 | else: 642 | tag = self.install_url.split("/")[-1].split("-")[1].split(".")[0] 643 | # print(tag) 644 | return tag 645 | 646 | class Updater(QWidget): 647 | def __init__(self): 648 | super().__init__() 649 | # self.name = plugin_name 650 | self.setWindowTitle("Update") 651 | self.setGeometry(100, 100, 500, 200) 652 | 653 | 654 | # self.button = QPushButton("Uninstall") 655 | # self.buttonBox.addButton("Uninstall", QDialogButtonBox.ButtonRole.AcceptRole) 656 | # self.buttonBox.addButton("Update to Latest", QDialogButtonBox.ButtonRole.RejectRole) 657 | # self.buttonBox.accepted.connect(self.accept) 658 | # self.buttonBox.centerButtons() 659 | # uninstall_button = QPushButton() 660 | # uninstall_button.addButton("Uninstall", QDialogButtonBox.ButtonRole.AcceptRole) 661 | self.layout = QVBoxLayout() 662 | self.createTable() 663 | self.update_button = QPushButton("Update to Latest") 664 | self.update_button.clicked.connect(self.update_plugin) 665 | # self.test_button = QPushButton("Test") 666 | # self.tableWidget.setCellWidget(1,0, self.buttonBox) 667 | # self.tableWidget.setCellWidget(1, 2, self.update_button) 668 | # self.tableWidget.setCellWidget(1, 1,self.test_button) 669 | # self.test_button.clicked.connect(self.test_plugin) 670 | # message = QLabel("Do you want to uninstall?") 671 | # self.layout.addWidget(message) 672 | self.layout.addWidget(self.tableWidget) 673 | self.layout.addWidget(self.update_button) 674 | # self.layout.addWidget(self.buttonBox) 675 | self.setLayout(self.layout) 676 | 677 | def test_plugin(self): 678 | self.tableWidget.setItem(0, 1, QTableWidgetItem("Testing...")) 679 | 680 | 681 | def update_plugin(self): 682 | if len(self.tag_list) == 0: 683 | print("No versioning") 684 | return 685 | version = self.update_button.text().split()[-1] 686 | if version == "Latest": 687 | version = self.tag_list[0] 688 | print("Updating to version", version) 689 | 690 | r = client.get(f"http://127.0.0.1:8000/plugin_manager/update/DeepMake/{version}") 691 | # print(p.communicate()) 692 | current_tag = self.getVersion() 693 | cur_ver_item = QTableWidgetItem(f"Current Version: {current_tag}") 694 | cur_ver_item.setFlags(Qt.ItemIsEnabled) 695 | self.tableWidget.setItem(0, 0, cur_ver_item) 696 | print("Updated to version", version) 697 | 698 | def version_management(self): 699 | tag_list = [] 700 | tags = subprocess.check_output("git tag".split()).decode("utf-8") 701 | for label in tags.split("\n")[:-1]: 702 | tag_list.append(label) 703 | print(tag_list) 704 | return tag_list 705 | 706 | def createTable(self): 707 | self.tableWidget = QTableWidget() 708 | row_count = 2 709 | column_count = 3 710 | 711 | self.tableWidget.setRowCount(row_count) 712 | self.tableWidget.setColumnCount(column_count) 713 | self.button_dict = {} 714 | current_tag = self.getVersion() 715 | 716 | cur_ver_item = QTableWidgetItem(f"Current Version: {current_tag}") 717 | avail_item = QTableWidgetItem("Available Versions") 718 | cur_ver_item.setFlags(Qt.ItemIsEnabled) 719 | avail_item.setFlags(Qt.ItemIsEnabled) 720 | 721 | self.tableWidget.setItem(0, 0, cur_ver_item) 722 | self.tableWidget.setItem(0, 1, avail_item) 723 | dropdown = QComboBox() 724 | self.tag_list = self.version_management() 725 | self.tag_list.reverse() 726 | dropdown.addItems(self.tag_list) 727 | dropdown.currentTextChanged.connect(self.changeVersion) 728 | 729 | self.tableWidget.setCellWidget(0, 2, dropdown) 730 | 731 | self.tableWidget.setShowGrid(False) 732 | self.tableWidget.horizontalHeader().setVisible(False) 733 | self.tableWidget.verticalHeader().setVisible(False) 734 | # self.tableWidget.setHorizontalHeaderLabels(['Name', 'Description', 'Version', 'Install']) 735 | 736 | self.tableWidget.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) 737 | self.tableWidget.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) 738 | self.tableWidget.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) 739 | def changeVersion(self, text): 740 | self.update_button.setText(f"Update to {text}") 741 | 742 | def getVersion(self): 743 | try: 744 | tag = subprocess.check_output("git describe --tags".split()).decode("utf-8").split("\n")[0] 745 | except: 746 | tag = "0.0.0" 747 | print(tag) 748 | # print(tag) 749 | return tag 750 | 751 | class ReportIssueDialog(QWidget): 752 | def __init__(self, logFilePath=None): 753 | super().__init__() 754 | self.logFilePath = logFilePath 755 | print(f"Initialized with logFilePath: {self.logFilePath}") 756 | self.initUI() 757 | 758 | def initUI(self): 759 | self.setWindowTitle('Report Issue') 760 | self.setGeometry(100, 100, 600, 300) 761 | layout = QVBoxLayout() 762 | 763 | layout.addWidget(QLabel('Error information:')) 764 | self.errorInfoTextEdit = QTextEdit() 765 | self.errorInfoTextEdit.setPlaceholderText("Describe the error or issue...") 766 | layout.addWidget(self.errorInfoTextEdit) 767 | 768 | layout.addWidget(QLabel('Additional information to include:')) 769 | self.additionalInfoTextEdit = QTextEdit() 770 | self.additionalInfoTextEdit.setPlaceholderText('Any extra details to help understand the issue...') 771 | layout.addWidget(self.additionalInfoTextEdit) 772 | 773 | self.attachLogCheckbox = QCheckBox("Attach log file") 774 | if self.logFilePath and os.path.exists(self.logFilePath) and os.path.getsize(self.logFilePath) > 0: 775 | self.attachLogCheckbox.setVisible(True) 776 | else: 777 | self.attachLogCheckbox.setVisible(False) 778 | layout.addWidget(self.attachLogCheckbox) 779 | 780 | layout.addWidget(QLabel('For immediate support you can join our Discord:')) 781 | self.discordButton = QPushButton() 782 | self.discordButton.setIcon(QIcon('Discord.png')) 783 | self.discordButton.setIconSize(QPixmap('Discord.png').size()) 784 | self.discordButton.clicked.connect(self.joinDiscord) 785 | layout.addWidget(self.discordButton) 786 | 787 | self.sendReportButton = QPushButton('Send') 788 | self.sendReportButton.clicked.connect(self.sendReport) 789 | layout.addWidget(self.sendReportButton) 790 | 791 | self.setLayout(layout) 792 | 793 | def sendReport(self): 794 | errorInfo = self.errorInfoTextEdit.toPlainText() 795 | additionalInfo = self.additionalInfoTextEdit.toPlainText() 796 | attachLog = self.attachLogCheckbox.isChecked() 797 | print(f"Sending report with error info: '{errorInfo}' and additional info: '{additionalInfo}', attach log: {attachLog}") 798 | 799 | data = {'description': f"Error Info: {errorInfo}\nAdditional Info: {additionalInfo}"} 800 | print(data) 801 | if attachLog and self.logFilePath: 802 | print(f"Attaching log file at: {self.logFilePath}") 803 | data['log_file_path'] = self.logFilePath 804 | 805 | try: 806 | response = requests.post("http://127.0.0.1:8000/report/report", data=data) 807 | print("Response Status Code:", response.status_code) 808 | if response.ok: 809 | print("Report sent successfully.") 810 | else: 811 | print(f"Failed to send report. Status code: {response.status_code}") 812 | except requests.RequestException as e: 813 | print(f"Error sending report: {e}") 814 | finally: 815 | self.close() 816 | 817 | def joinDiscord(self): 818 | webbrowser.open('https://discord.gg/U7FymgCM') 819 | 820 | 821 | class LoginWidget(QWidget): 822 | def __init__(self): 823 | super().__init__() 824 | self.backend_url = "http://localhost:8000/login" 825 | self.initUI() 826 | self.check_login() 827 | 828 | def initUI(self): 829 | self.layout = QVBoxLayout() 830 | 831 | # Login Section 832 | self.loginSection = QVBoxLayout() 833 | loginTitle = QLabel('Login to Deepmake') 834 | loginTitle.setStyleSheet('font-size: 18px; font-weight: bold;') 835 | 836 | emailLayout = QHBoxLayout() 837 | emailLabel = QLabel('Email') 838 | self.emailInput = QLineEdit() 839 | self.emailInput.setPlaceholderText('Enter your Email') 840 | emailLayout.addWidget(emailLabel) 841 | emailLayout.addWidget(self.emailInput) 842 | 843 | passwordLayout = QHBoxLayout() 844 | passwordLabel = QLabel('Password') 845 | self.passwordInput = QLineEdit() 846 | self.passwordInput.setPlaceholderText('Enter your Password') 847 | self.passwordInput.setEchoMode(QLineEdit.Password) 848 | passwordLayout.addWidget(passwordLabel) 849 | passwordLayout.addWidget(self.passwordInput) 850 | 851 | self.loginButton = QPushButton('Login') 852 | self.loginButton.clicked.connect(self.login) 853 | 854 | self.loginSection.addWidget(loginTitle) 855 | self.loginSection.addLayout(emailLayout) 856 | self.loginSection.addLayout(passwordLayout) 857 | self.loginSection.addWidget(self.loginButton) 858 | 859 | # Logged In Section 860 | self.loggedInSection = QVBoxLayout() 861 | self.loggedInLabel = QLabel('Logged in as ') 862 | self.loggedInLabel.setVisible(False) # Hide until logged in 863 | self.logoutButton = QPushButton('Logout') 864 | self.logoutButton.clicked.connect(self.logout) 865 | self.logoutButton.setVisible(False) # Hide until logged in 866 | 867 | self.loggedInSection.addWidget(self.loggedInLabel) 868 | self.loggedInSection.addWidget(self.logoutButton) 869 | 870 | # Add sections to the main layout 871 | self.layout.addLayout(self.loginSection) 872 | self.layout.addLayout(self.loggedInSection) 873 | self.setLayout(self.layout) 874 | 875 | def check_login(self): 876 | try: 877 | response = requests.get(f"{self.backend_url}/status") 878 | if response.status_code == 200 and response.json().get('logged_in', False): 879 | self.loggedInLabel.setText(f'Logged in as {response.json().get("username", "Unknown")}') 880 | self.toggleLoginState(True) 881 | except requests.exceptions.RequestException as e: 882 | QMessageBox.critical(self, "Connection Error", f"Failed to check login status: {str(e)}") 883 | 884 | def login(self): 885 | email = self.emailInput.text() 886 | password = self.passwordInput.text() 887 | try: 888 | response = requests.post(f"{self.backend_url}/login", json={"username": email, "password": password}) 889 | if response.status_code == 200: 890 | response_data = response.json() 891 | if response_data['status'] == 'success': 892 | self.loggedInLabel.setText(f'Logged in as {response_data.get("username", email)}') 893 | self.toggleLoginState(True) 894 | else: 895 | QMessageBox.warning(self, "Login Failed", response_data.get("message", "Login failed. Please check your credentials.")) 896 | else: 897 | QMessageBox.warning(self, "Login Failed", "Login failed. Please check your credentials.") 898 | except requests.exceptions.RequestException as e: 899 | QMessageBox.critical(self, "Connection Error", f"Login failed: {str(e)}") 900 | 901 | def logout(self): 902 | try: 903 | response = requests.get(f"{self.backend_url}/logout") 904 | if response.status_code == 200: 905 | self.toggleLoginState(False) 906 | except requests.exceptions.RequestException as e: 907 | QMessageBox.critical(self, "Connection Error", f"Logout failed: {str(e)}") 908 | 909 | def toggleLoginState(self, loggedIn): 910 | self.loginSection.setEnabled(not loggedIn) 911 | self.loggedInLabel.setVisible(loggedIn) 912 | self.logoutButton.setVisible(loggedIn) 913 | if not loggedIn: 914 | self.loggedInLabel.setText('') 915 | self.emailInput.clear() 916 | self.passwordInput.clear() --------------------------------------------------------------------------------