diff --git a/DeepLXTranslateNode/.gitignore b/DeepLXTranslateNode/.gitignore deleted file mode 100644 index a0668f0..0000000 --- a/DeepLXTranslateNode/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -go/ -DeepLX/ -config.json \ No newline at end of file diff --git a/DeepLXTranslateNode/README.md b/DeepLXTranslateNode/README.md deleted file mode 100644 index 5422d81..0000000 --- a/DeepLXTranslateNode/README.md +++ /dev/null @@ -1,134 +0,0 @@ -# DeepLX nodes for ComfyUI, for translating prompts into other languages. - -Powerful Free DeepL API, No Token Required. Used module [DeepLX](https://github.com/OwO-Network/DeepLX) . - -### Languages supported: - -[List languages supported](https://developers.deepl.com/docs/getting-started/supported-languages) - -
- Source Languages - -- Arabic: AR -- Bulgarian: BG -- Czech: CS -- Danish: DA -- German: DE -- Greek: EL -- English: EN -- Spanish: ES -- Estonian: ET -- Finnish: FI -- French: FR -- Hungarian: HU -- Indonesian: ID -- Italian: IT -- Japanese: JA -- Korean: KO -- Lithuanian: LT -- Latvian: LV -- Norwegian Bokmål: NB -- Dutch: NL -- Polish: PL -- Portuguese: PT -- Romanian: RO -- Russian: RU -- Slovak: SK -- Slovenian: SL -- Swedish: SV -- Turkish: TR -- Ukrainian: UK -- Chinese: ZH - -
- -
- Target Languages - -- Arabic: AR -- Bulgarian: BG -- Czech: CS -- Danish: DA -- German: DE -- Greek: EL -- English: EN -- English (British): EN-GB -- English (American): EN-US -- Spanish: ES -- Estonian: ET -- Finnish: FI -- French: FR -- Hungarian: HU -- Indonesian: ID -- Italian: IT -- Japanese: JA -- Korean: KO -- Lithuanian: LT -- Latvian: LV -- Norwegian Bokmål: NB -- Dutch: NL -- Polish: PL -- Portuguese: PT -- Portuguese (Brazilian): PT-BR -- Portuguese (all Portuguese variants excluding Brazilian Portuguese): PT-PT -- Romanian: RO -- Russian: RU -- Slovak: SK -- Slovenian: SL -- Swedish: SV -- Turkish: TR -- Ukrainian: UK -- Chinese (simplified): ZH-HANS -- Chinese (traditional): ZH-HANT - -
- -### Autoinstaller - -**Golang (Go)** and **DeepLX** installation occurs automatically if the **Go** or **DeepLX** directory is missing. - -You can also call the automatic installer by running shell `run_install_deeplx_windows.cmd` or `run_install_deeplx_linux.sh` for your platform. - -**_Note: If you encounter any problems, you can use manual installation._** - -### Manual install and use: - -1. Install my custom nodes in ComfyUI, used [HERE](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet#installing) -2. Download zip archive Golang (programming language) from your system (Widnows, Linux ...) [Download Golang](https://go.dev/dl/) -3. Extract archive (inside archive folder `go`) in the folder `\ComfyUI\custom_nodes\ComfyUI_Custom_Nodes_AlekPet\DeepLXTranslateNode` -4. Open shell in the folder `\ComfyUI\custom_nodes\ComfyUI_Custom_Nodes_AlekPet\DeepLXTranslateNode` and run command `git clone https://github.com/OwO-Network/DeepLX` (you need install Git if not exists [Git](https://git-scm.com/downloads)) - -**Structure folders:** - -![DeepLXTranslateNode structures folders](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/raw/master/DeepLXTranslateNode/image_deeplx_structures_folders.png) - -5. Run comfyui (**see note after**) and add node `DeepLXTranslateCLIPTextEncodeNode` or `DeepLXTranslateTextNode` context menu in the category `CONDITIONING`. -6. [Optional] If you are in **CH**, you need in the `\ComfyUI\custom_nodes\ComfyUI_Custom_Nodes_AlekPet\DeepLXTranslateNode\go`, edit file **go.env**, modify this line `GOPROXY=https://proxy.golang.org,direct` to `GOPROXY=https://goproxy.cn,direct` [Thanks tongpeng1988](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/issues/136#issuecomment-2804126821) . - -**_Note: The first launch may fail, Golang is installing packages for DeepLX, just restart ComfyUI!_** - -### Note - -On first launch, a `config.json` file will be created where you can specify the initial values ​​from which language to translate and to which language. You can also disable DeepLXTranslate nodes using the 'run_deeplx' property. -**Default config.json values:** - -```json -{ - "settings": { - "__commnet": "Please check the list of available languages ​​before specifying, especially target_lang! Property run_deeplx disable DeepLXTranslate nodes. See README file", - "source_lang": "Russian", - "target_lang": "English", - "run_deeplx": true - } -} -``` - -> Includes: - -> **DeepLXTranslateCLIPTextEncodeNode** - translate text, and return CONDITIONING -> -> **DeepLXTranslateTextNode** - translate text and return text (STRING) - -## Image: - -![DeepLXTranslateNode](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/raw/master/DeepLXTranslateNode/image_deeplx_translate_node.jpg) diff --git a/DeepLXTranslateNode/deeplx_translate_node.py b/DeepLXTranslateNode/deeplx_translate_node.py deleted file mode 100644 index 10d5c90..0000000 --- a/DeepLXTranslateNode/deeplx_translate_node.py +++ /dev/null @@ -1,386 +0,0 @@ -# Title: DeepLX Translate promts nodes -# Author: AlekPet (https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet) -# Github DeepLX: https://github.com/OwO-Network/DeepLX - -import os -import json -import subprocess -import requests -import time -from . import install_deeplx - - -LANGUAGES_CODES = { - "source": { - "Arabic": "AR", - "Bulgarian": "BG", - "Czech": "CS", - "Danish": "DA", - "German": "DE", - "Greek": "EL", - "English": "EN", - "Spanish": "ES", - "Estonian": "ET", - "Finnish": "FI", - "French": "FR", - "Hungarian": "HU", - "Indonesian": "ID", - "Italian": "IT", - "Japanese": "JA", - "Korean": "KO", - "Lithuanian": "LT", - "Latvian": "LV", - "Norwegian Bokmål": "NB", - "Dutch": "NL", - "Polish": "PL", - "Portuguese": "PT", - "Romanian": "RO", - "Russian": "RU", - "Slovak": "SK", - "Slovenian": "SL", - "Swedish": "SV", - "Turkish": "TR", - "Ukrainian": "UK", - "Chinese": "ZH", - }, - "target": { - "Arabic": "AR", - "Bulgarian": "BG", - "Czech": "CS", - "Danish": "DA", - "German": "DE", - "Greek": "EL", - "English": "EN", - "English (British)": "EN-GB", - "English (American)": "EN-US", - "Spanish": "ES", - "Estonian": "ET", - "Finnish": "FI", - "French": "FR", - "Hungarian": "HU", - "Indonesian": "ID", - "Italian": "IT", - "Japanese": "JA", - "Korean": "KO", - "Lithuanian": "LT", - "Latvian": "LV", - "Norwegian Bokmål": "NB", - "Dutch": "NL", - "Polish": "PL", - "Portuguese": "PT", - "Portuguese (Brazilian)": "PT-BR", - "Portuguese (all Portuguese variants excluding Brazilian Portuguese)": "PT-PT", - "Romanian": "RO", - "Russian": "RU", - "Slovak": "SK", - "Slovenian": "SL", - "Swedish": "SV", - "Turkish": "TR", - "Ukrainian": "UK", - "Chinese (simplified)": "ZH-HANS", - "Chinese (traditional)": "ZH-HANT", - }, -} - - -# Colors -class ColPrint: - RED = "\033[1;31;40m" - GREEN = "\033[92m" - YELLOW = "\033[93m" - MAGNETA = "\033[95m" - BLUE = "\033[94m" - CLEAR = "\033[0m" - - -# Node directory -NODE_DIR = os.path.dirname(os.path.abspath(__file__)) -SETTINGS = {} - -# Directory node and config file -config_path = os.path.join(NODE_DIR, "config.json") - -# Load config.js file -if not os.path.exists(config_path): - print( - f"{ColPrint.YELLOW}[DeepLXTranslateNode] {ColPrint.MAGNETA}File config.js file not found! Create default!{ColPrint.CLEAR}" - ) - - with open(config_path, "w", encoding="utf-8") as f: - defaultJSON = { - "settings": { - "__commnet": "Please check the list of available languages ​​before specifying, especially target_lang! Property run_deeplx disable DeepLXTranslate nodes. See README file", - "source_lang": "Russian", - "target_lang": "English", - "run_deeplx": True - } - } - json.dump(defaultJSON, f, ensure_ascii=False, indent=4) - -else: - with open(config_path, "r", encoding="utf-8") as f: - config = json.load(f) - SETTINGS = config.get("settings", {}) - - if SETTINGS.keys(): - reset_lang = False - - # Getting user settings for config.json - source_lang = SETTINGS.get("source_lang") - target_lang = SETTINGS.get("target_lang") - - if source_lang not in LANGUAGES_CODES["source"]: - SETTINGS.update({"source_lang": "Russian"}) - reset_lang = True - - if target_lang not in LANGUAGES_CODES["target"]: - SETTINGS.update({"target_lang": "English"}) - reset_lang = True - - if reset_lang: - print( - f"{ColPrint.YELLOW}[DeepLXTranslateNode]{ColPrint.MAGNETA} Source or target language not found in list of available languages, check config.js!{ColPrint.CLEAR}" - ) - - -# DeepLX and go constants -DEEPLX_SERVER_RUNNING = False -DEEPLX_SERVER_URL = "http://127.0.0.1:1188/" -DEEPLX_SERVER_URL_TRANSLATE = "http://127.0.0.1:1188/translate" -RETRY_CHECK_SERVER = 10 - -PATH_TO_DEEPLX_SERVER = os.path.join(NODE_DIR, "DeepLX") -PATH_TO_GO = os.path.join(NODE_DIR, "go", "bin") - -# We check whether the user has disabled the launch of the DeepLX server. -if not SETTINGS.get("run_deeplx", True): # Setting whether to run DeepLX server - DEEPLX_SERVER_RUNNING = False - raise Exception("User disabled DeepLX server startup in config.json (proprety: 'run_deeplx')") - - -### Automatical install Golang (Go) and DeepLX ### -try: - # Checking exists path to Go and install Golang (Go) - if not os.path.exists(PATH_TO_GO): - print(f"{ColPrint.YELLOW}[DeepLXTranslateNode]{ColPrint.MAGNETA} Path to 'Golang (Go)' not exists, try install...{ColPrint.CLEAR}") - - # Get platform - platform = install_deeplx.getPlatform() - - if platform is not None: - print(f"{ColPrint.YELLOW}[DeepLXTranslateNode]{ColPrint.GREEN} Your platform is: {'macOS (Darwin)' if platform == 'darwin' else platform.capitalize()}{ColPrint.CLEAR}") - # Get link - go_url = install_deeplx.GO_URLS[platform] - - # Download file - file_name, path_to_file = install_deeplx.download(go_url, NODE_DIR) - - # Extract file - install_deeplx.extractArchive(path_to_file) - else: - print(f"{ColPrint.RED}[DeepLXTranslateNode] Unable to determine identifying the underlying platform! Use manual installation!{ColPrint.CLEAR} ") - - - # # Checking exists path to Go and install DeepLX - if not os.path.exists(PATH_TO_DEEPLX_SERVER): - print( - f"{ColPrint.YELLOW}[DeepLXTranslateNode]{ColPrint.MAGNETA} Path to DeepLX server folder not exists, try install...{ColPrint.CLEAR}") - - # Get link - deeplx_url = install_deeplx.FILES_DOWNLOAD["DeepLX"].get("url") - - # Download file - file_name, path_to_file = install_deeplx.download(deeplx_url, NODE_DIR) - - # Extract file - install_deeplx.extractArchive(path_to_file) - - # Rename DeepLX-master to DeepLX - install_deeplx.otherOperations("DeepLX", NODE_DIR) - -except Exception as e: - raise e - - - -### end - Automatical install Golang (Go) and DeepLX ### - -# Checking pathes variables -if not os.path.exists(PATH_TO_GO): - raise FileNotFoundError(f"Error path to 'Golang (Go)' not exists: {PATH_TO_GO}") - - -if not os.path.exists(PATH_TO_DEEPLX_SERVER): - raise FileNotFoundError(f"Error path to DeepLX server folder not exists: {PATH_TO_DEEPLX_SERVER}") - - -# Detect platform -go_executable = os.path.join(PATH_TO_GO, "go") -if os.name == "nt": - go_executable += ".exe" - -# Try run DeepLX server -try: - print(f"{ColPrint.YELLOW}[DeepLXTranslateNode] {ColPrint.BLUE}Running server DeepLX...{ColPrint.CLEAR}") - proc_deeplx = subprocess.Popen([go_executable, "run", "main.go"], cwd=PATH_TO_DEEPLX_SERVER) - DEEPLX_SERVER_RUNNING = True - time.sleep(2) - -except Exception as e: - DEEPLX_SERVER_RUNNING = False - raise Exception( - f"{ColPrint.RED}[DeepLXTranslateNode] Error running server DeepLX: {ColPrint.MAGNETA}{e}{ColPrint.CLEAR}" - ) - -if DEEPLX_SERVER_RUNNING: - for retry in range(RETRY_CHECK_SERVER, 0, -1): - print( - f"{ColPrint.YELLOW}[DeepLXTranslateNode] {ColPrint.BLUE}Checking DeepLX server. Attempt {retry}...{ColPrint.CLEAR}" - ) - - try: - response = requests.get(DEEPLX_SERVER_URL) - response.raise_for_status() - - print( - f"{ColPrint.YELLOW}[DeepLXTranslateNode]{ColPrint.GREEN} Server responded successfully: {ColPrint.CLEAR}", - response.text, - ) - DEEPLX_SERVER_RUNNING = True - break - - except requests.HTTPError as e: - print( - f"{ColPrint.RED}[DeepLXTranslateNode] HTTP error while connecting to DeepLX: {ColPrint.MAGNETA}{e}{ColPrint.CLEAR}" - ) - if retry == 1: - DEEPLX_SERVER_RUNNING = False - raise - - except Exception as e: - if retry == 1: - DEEPLX_SERVER_RUNNING = False - raise Exception( - f"{ColPrint.RED}[DeepLXTranslateNode] Unexpected error while checking DeepLX server: {ColPrint.MAGNETA}{e}{ColPrint.CLEAR}" - ) - - time.sleep(2) - - else: - DEEPLX_SERVER_RUNNING = False - print( - f"{ColPrint.RED}[DeepLXTranslateNode] All attempts to reach the DeepLX server have failed.{ColPrint.CLEAR}" - ) - - -def createRequest(payload): - """Request function""" - try: - response = requests.post(DEEPLX_SERVER_URL_TRANSLATE, json=payload) - response.raise_for_status() - - if response.status_code == 200: - json_data = response.json() - response_text = json_data.get("data").strip() - - return response_text - - except requests.HTTPError as e: - print( - f"DeepLX server is not running! Please check Go and DeepLX repository installation or information in the ComfyUI terminal!: {response.status_code}, {response.text}" - ) - raise e - except Exception as e: - print(f"Error DeepLX: {e}") - raise e - - -def translate(prompt, source, target): - """Translate function""" - # Check prompt exist - if prompt is None or prompt.strip() == "": - return "" - - # Create body request - payload = {"source_lang": source, "target_lang": target, "text": prompt} - - response_translate_text = createRequest(payload) - - return response_translate_text - - -class DeepLXTranslateCLIPTextEncodeNode: - @classmethod - def INPUT_TYPES(self): - source_langs = list(LANGUAGES_CODES.get("source").keys()) - target_langs = list(LANGUAGES_CODES.get("target").keys()) - - return { - "required": { - "source_translate": ( - source_langs, - {"default": SETTINGS.get("source_lang", "Russian"), "tooltip": "Translation source languge"}, - ), - "target_translate": ( - target_langs, - {"default": SETTINGS.get("target_lang", "English"), "tooltip": "Translation target languge"}, - ), - "prompt": ( - "STRING", - {"multiline": True, "placeholder": "Input prompt text"}, - ), - "clip": ("CLIP",), - } - } - - RETURN_TYPES = ( - "CONDITIONING", - "STRING", - ) - FUNCTION = "deeplx_translate_text" - DESCRIPTION = "This is a node that translates the prompt into another language using DeepLX." - CATEGORY = "AlekPet Nodes/conditioning" - - def deeplx_translate_text(self, source_translate, target_translate, prompt, clip): - if not DEEPLX_SERVER_RUNNING: - raise ValueError( - "DeepLX server is not running! Please check Go and DeepLX repository installation or information in the ComfyUI terminal!" - ) - - # Get short value source and target code languages - source_translate_code = LANGUAGES_CODES["source"][source_translate] - target_translate_code = LANGUAGES_CODES["target"][target_translate] - - prompt = translate(prompt, source_translate_code, target_translate_code) - - tokens = clip.tokenize(prompt) - cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True) - return ([[cond, {"pooled_output": pooled}]], prompt) - - -class DeepLXTranslateTextNode(DeepLXTranslateCLIPTextEncodeNode): - @classmethod - def INPUT_TYPES(self): - return_types = super().INPUT_TYPES() - del return_types["required"]["clip"] - return return_types - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ("text",) - FUNCTION = "deeplx_translate_text" - - CATEGORY = "AlekPet Nodes/text" - - def deeplx_translate_text(self, source_translate, target_translate, prompt): - if not DEEPLX_SERVER_RUNNING: - raise ValueError( - "DeepLX server is not running! Please check Go and DeepLX repository installation or information in the ComfyUI terminal!" - ) - - # Get short value source and target code languages - source_translate_code = LANGUAGES_CODES["source"][source_translate] - target_translate_code = LANGUAGES_CODES["target"][target_translate] - - prompt = translate(prompt, source_translate_code, target_translate_code) - - return (prompt,) diff --git a/DeepLXTranslateNode/image_deeplx_structures_folders.png b/DeepLXTranslateNode/image_deeplx_structures_folders.png deleted file mode 100644 index 7dadaff..0000000 Binary files a/DeepLXTranslateNode/image_deeplx_structures_folders.png and /dev/null differ diff --git a/DeepLXTranslateNode/image_deeplx_translate_node.jpg b/DeepLXTranslateNode/image_deeplx_translate_node.jpg deleted file mode 100644 index 3f2764d..0000000 Binary files a/DeepLXTranslateNode/image_deeplx_translate_node.jpg and /dev/null differ diff --git a/DeepLXTranslateNode/install_deeplx.py b/DeepLXTranslateNode/install_deeplx.py deleted file mode 100644 index f3649ec..0000000 --- a/DeepLXTranslateNode/install_deeplx.py +++ /dev/null @@ -1,219 +0,0 @@ -# Title: Autoinstall Golang (Go) and DeepLX for ComfyUI DeepLXTranslateNode -# Author: AlekPet (https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet) -# DeepLXTranslateNode README: https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/blob/master/DeepLXTranslateNode/README.md - -import os -from sys import version_info, platform as _platform -import platform -import requests -from tqdm import tqdm -import shutil - -class ColPrintInstall: - RED = "\033[1;31;40m" - GREEN = "\033[92m" - YELLOW = "\033[93m" - MAGNETA = "\033[95m" - BLUE = "\033[94m" - CLEAR = "\033[0m" - - -GO_URLS = { - "win32": "https://go.dev/dl/go1.24.2.windows-386.zip", - "win64": "https://go.dev/dl/go1.24.2.windows-amd64.zip", - "darwin": "https://go.dev/dl/go1.24.2.darwin-arm64.tar.gz", - "linux_i386_i686": "https://go.dev/dl/go1.24.2.linux-386.tar.gz", - "linux_x86_64": "https://go.dev/dl/go1.24.3.linux-amd64.tar.gz", - "linux_aarch64": "https://go.dev/dl/go1.24.3.linux-arm64.tar.gz", - "linux_armv6l": "https://go.dev/dl/go1.24.3.linux-armv6l.tar.gz", - } - -FILES_DOWNLOAD = { - "Golang": { - "url": None, - }, - "DeepLX": { - "url":"https://github.com/OwO-Network/DeepLX/archive/refs/heads/main.zip", - }, -} - -WORK_DIR = os.path.dirname(os.path.abspath(__file__)) - - -def getPlatform(): - """ Get platform machine """ - platform_name = None - - if _platform.startswith("linux"): - # Check architecture linux - arch = platform.uname() - if arch.machine == "x86_64": - platform_name = "linux_x86_64" - - elif arch.machine == "i386" or arch.machine == "i686": - platform_name = "linux_i386_i686" - - elif arch.machine == "aarch64": - platform_name = "linux_aarch64" - - elif arch.machine == "armv6l" or arch.machine == "armv7l": - platform_name = "linux_armv6l" - - else: - platform_name = "linux_i386_i686" - - elif _platform == "darwin": - platform_name = "darwin" - - elif _platform == "win32": - # Check Windows id 64-bit - if platform.machine().endswith('64'): - platform_name = "win64" - else: - platform_name = "win32" - - return platform_name - - -def download(url, save_to = WORK_DIR): - """ Download file """ - file_name = url.split('/')[-1] - print(f"{ColPrintInstall.YELLOW}[DeepLXTranslateNode]{ColPrintInstall.BLUE} Downloading file: {url}{ColPrintInstall.CLEAR}") - - response = requests.get(url, stream=True) - file_size = int(response.headers.get('Content-Length', 0)) - - save_file_path = os.path.join(save_to, file_name) - chunk_size = 1024 - - with open(save_file_path, "wb") as handle, tqdm( - desc=file_name, - total=file_size, - unit='B', - unit_scale=True, - unit_divisor=chunk_size, -) as progress_bar: - for data in response.iter_content(chunk_size=chunk_size): - handle.write(data) - progress_bar.update(len(data)) - - - print(f"{ColPrintInstall.YELLOW}[DeepLXTranslateNode]{ColPrintInstall.GREEN} File '{file_name}' downloaded!{ColPrintInstall.CLEAR}") - return file_name, save_file_path - - -def extractArchive(file_path, extract_to = WORK_DIR, removeArchive = True): - """ Extract zip or tar.gzip archive """ - try: - file_name = os.path.basename(file_path) - ext = os.path.splitext(file_path)[-1] - desc = f"Extracting '{file_name}'" - - # Type archive - if ext == ".gz": - # TarFile - import tarfile - - # Safe extract for python < 3.12 (secure path traversal) - def safe_extract(tar, path=".", members=None): - if members is None: - members = tar.getmembers() - - safe_members = [] - for member in members: - member_path = os.path.join(path, member.name) - if not os.path.abspath(member_path).startswith(os.path.abspath(path)): - continue - safe_members.append(member) - - tar.extractall(path, safe_members) - - # Open tar archive - with tarfile.open(file_path, 'r') as tar_archive: - members = tar_archive.getmembers() - - # Python >= 3.12 - if version_info >= (3, 12): - for file in tqdm(members, desc=desc, unit="file"): - tar_archive.extract(file, extract_to, filter='data') - else: - for member in tqdm(members, desc=desc): - safe_extract(tar_archive, extract_to, [member]) - - - elif ext == ".zip": - # ZipFile - import zipfile - - # Open zip archive - with zipfile.ZipFile(file_path, 'r') as zip_archive: - file_list = zip_archive.namelist() - - with tqdm(file_list, desc=desc, unit="file") as pbar: - for file in pbar: - zip_archive.extract(file, extract_to) - - print(f"{ColPrintInstall.YELLOW}[DeepLXTranslateNode]{ColPrintInstall.GREEN} Archive '{file_name}' extracted.{ColPrintInstall.CLEAR}") - - # Remove archive file - if removeArchive: - os.remove(file_path) - print(f"{ColPrintInstall.YELLOW}[DeepLXTranslateNode]{ColPrintInstall.MAGNETA} File {file_name} removed, after extract.{ColPrintInstall.CLEAR}\n") - - except Exception as e: - raise e - - -def otherOperations(name, save_path_dir): - if name == "DeepLX": - path_to_deeplx_master = os.path.join(save_path_dir, "DeepLX-main") - path_to_rename = os.path.join(save_path_dir, "DeepLX") - - if os.path.isdir(path_to_deeplx_master) and not os.path.exists(path_to_rename): - os.rename(path_to_deeplx_master, path_to_rename) - print(f"{ColPrintInstall.YELLOW}[DeepLXTranslateNode]{ColPrintInstall.MAGNETA} Directory 'DeepLX-main' renamed to 'DeepLX'!{ColPrintInstall.CLEAR}") - else: - print(f"{ColPrintInstall.YELLOW}[DeepLXTranslateNode]{ColPrintInstall.MAGNETA} Directory 'DeepLX' already exists or directory {path_to_deeplx_master}' is not correct! Renaming operation abort!{ColPrintInstall.CLEAR}") - shutil.rmtree(path_to_deeplx_master, ignore_errors=True) - - -def main(): - print(f"""{ColPrintInstall.MAGNETA}~~~~~~~~~ Autoinstall Golang (Go) and DeepLX for ComfyUI DeepLXTranslateNode by AlekPet ~~~~~~~~~ -#{ColPrintInstall.CLEAR} Github: {ColPrintInstall.BLUE}https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet -{ColPrintInstall.MAGNETA}~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{ColPrintInstall.CLEAR}""") - try: - platform_name = getPlatform() - print(f"{ColPrintInstall.YELLOW}[DeepLXTranslateNode]{ColPrintInstall.GREEN} Your platform is: {'macOS (Darwin)' if platform_name == 'darwin' else platform_name.capitalize()}{ColPrintInstall.CLEAR}") - - if platform_name is None: - raise Exception("Unable to determine identifying the underlying platform!") - - url_down = GO_URLS[platform_name] - - # Download links (Golang and DeepLX) - FILES_DOWNLOAD["Golang"]["url"] = url_down - - for keyName in FILES_DOWNLOAD: - url = FILES_DOWNLOAD[keyName].get("url") - - if url is None: - continue - - # Download file - file_name, path_to_file = download(url, WORK_DIR) - - # Extract file - extractArchive(path_to_file) - - # Other operation - otherOperations(keyName, WORK_DIR) - - - print(f"{ColPrintInstall.GREEN}--------------------\nAutoinstall has completed its work...{ColPrintInstall.CLEAR}") - - except Exception as e: - raise e - -if __name__ == "__main__": - main() - diff --git a/DeepLXTranslateNode/run_install_deeplx_linux.sh b/DeepLXTranslateNode/run_install_deeplx_linux.sh deleted file mode 100644 index 38f8c3d..0000000 --- a/DeepLXTranslateNode/run_install_deeplx_linux.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -# Install Golang and DeepLX for ComfyUI DeepLXTranslateNode by AlekPet -# Github repository: https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/DeepLXTranslateNode - -python install_deeplx.py \ No newline at end of file diff --git a/DeepLXTranslateNode/run_install_deeplx_windows.cmd b/DeepLXTranslateNode/run_install_deeplx_windows.cmd deleted file mode 100644 index bf9ead9..0000000 --- a/DeepLXTranslateNode/run_install_deeplx_windows.cmd +++ /dev/null @@ -1,6 +0,0 @@ -@echo off -REM ### Install Golang and DeepLX for ComfyUI DeepLXTranslateNode by AlekPet ### -REM ### Github repository: https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/DeepLXTranslateNode - -..\..\..\..\python_embeded\python.exe install_deeplx.py -pause \ No newline at end of file diff --git a/DeepTranslatorNode/js/deep_translator_node.js b/DeepTranslatorNode/js/deep_translator_node.js index e187043..b2d6bc8 100644 --- a/DeepTranslatorNode/js/deep_translator_node.js +++ b/DeepTranslatorNode/js/deep_translator_node.js @@ -188,7 +188,11 @@ function get_support_langs() { // Auth const auth_data = responseData?.auth_data; - if (widget_auth_data) { + if ( + widget_auth_data && + auth_data instanceof Object && + Object.keys(auth_data)?.length + ) { let placeholder = Object.keys(placeholders).find( (ph_key) => ph_key.includes(service) || ph_key === service ); diff --git a/ExtrasNode/css/extras_node_speech_recognition_style.css b/ExtrasNode/css/extras_node_speech_recognition_style.css new file mode 100644 index 0000000..12bcc86 --- /dev/null +++ b/ExtrasNode/css/extras_node_speech_recognition_style.css @@ -0,0 +1,130 @@ +.alekpet_extras_node_speechrecognition_box { + margin: 0px; + padding: 0px; + position: absolute; + text-align: center; + cursor: pointer; + font-size: 1rem; + display: flex; + justify-content: center; + flex-flow: column; + width: 45px; + font-family: monospace; + align-items: center; + margin-top: 3px; +} + +.alekpet_extras_node_speechrecognition_row { + position: relative; + display: flex; + gap: 3px; + justify-content: center; + align-items: center; +} + +.alekpet_extras_node_recognition_icon { + width: 8px; + height: 8px; + border: 1px solid var(--input-text); + background: transparent; + border-radius: 50%; + padding: 0; + margin: 0px 5px 0px 0px; + display: inline-block; +} + +.alekpet_extras_node_recognition_icon_active { + border: 1px solid var(--input-text); + background: var(--error-text); + border-radius: 0; +} + +.alekpet_extras_node_recognition_icon_box { + display: flex; + justify-content: center; + align-items: baseline; + gap: 3px; + transition: all 0.7s; + margin-top: 2px; +} + +.alekpet_extras_node_recognition_icon:hover { + background: var(--error-text); +} + +.alekpet_extras_node_recognition_aborted { + transition: all 0.3s; + font-size: 0.6rem; +} + +.alekpet_extras_node_recognition_aborted:hover { + color: var(--error-text); +} + +.alekpet_extras_node_info { + display: flex; + align-self: center; +} + +.alekpet_extras_node_speech_recognition_checkbox { + width: 10px; + height: 10px; + accent-color: var(--input-text); + padding: 0; + margin: 0; +} + +.alekpet_extras_node_speech_recognition_checkbox:not(:checked) { + opacity: 0.5; +} + +.alekpet_extras_node_recognition_save { + accent-color: var(--p-red-400); +} + +.alekpet_extras_node_speech_icon { + font-size: 0.6em; + opacity: 1; + line-height: 1; + margin-top: 2px; +} + +.alekpet_extras_node_speech_icon_playing { + opacity: 0.7; +} + +.alekpet_extras_node_speech_icon_playing::after { + content: "✖"; + position: absolute; + right: 3px; + color: var(--error-text); +} + +/* Settings */ +.alekpet_extras_node_speech_recognition_settings_props_label { + background: #646363; + color: #cdcbcb; + border-radius: 5px; + padding: 3px; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: nowrap; +} + +.alekpet_extras_node_speech_recognition_settings_props_label:hover { + background: darkgrey; + color: white; +} + +.alekpet_extras_node_speech_recognition_settings_props_input { + max-width: 10px; + padding: 0; + margin: 0; + outline: 0; +} + +.alekpet_extras_node_speech_recognition_settings_button { + display: flex; + font-size: 0.6rem; +} diff --git a/ExtrasNode/css/extras_node_styles.css b/ExtrasNode/css/extras_node_styles.css index 35d1690..6b719f4 100644 --- a/ExtrasNode/css/extras_node_styles.css +++ b/ExtrasNode/css/extras_node_styles.css @@ -1,134 +1,3 @@ -.alekpet_extras_node_speechrecognition_box { - margin: 0px; - padding: 0px; - position: absolute; - text-align: center; - cursor: pointer; - font-size: 1rem; - display: flex; - justify-content: center; - flex-flow: column; - width: 45px; - font-family: monospace; - align-items: center; - margin-top: 3px; -} - -.alekpet_extras_node_speechrecognition_row { - position: relative; - display: flex; - gap: 3px; - justify-content: center; - align-items: center; -} - -.alekpet_extras_node_recognition_icon { - width: 8px; - height: 8px; - border: 1px solid var(--input-text); - background: transparent; - border-radius: 50%; - padding: 0; - margin: 0px 5px 0px 0px; - display: inline-block; -} - -.alekpet_extras_node_recognition_icon_active { - border: 1px solid var(--input-text); - background: var(--error-text); - border-radius: 0; -} - -.alekpet_extras_node_recognition_icon_box { - display: flex; - justify-content: center; - align-items: baseline; - gap: 3px; - transition: all 0.7s; - margin-top: 2px; -} - -.alekpet_extras_node_recognition_icon:hover { - background: var(--error-text); -} - -.alekpet_extras_node_recognition_aborted { - transition: all 0.3s; - font-size: 0.6rem; -} - -.alekpet_extras_node_recognition_aborted:hover { - color: var(--error-text); -} - -.alekpet_extras_node_info { - display: flex; - align-self: center; -} - -.alekpet_extras_node_speech_recognition_checkbox { - width: 10px; - height: 10px; - accent-color: var(--input-text); - padding: 0; - margin: 0; -} - -.alekpet_extras_node_speech_recognition_checkbox:not(:checked) { - opacity: 0.5; -} - -.alekpet_extras_node_recognition_save { - accent-color: var(--p-red-400); -} - -.alekpet_extras_node_speech_icon { - font-size: 0.6em; - opacity: 1; - line-height: 1; - margin-top: 2px; -} - -.alekpet_extras_node_speech_icon_playing { - opacity: 0.7; -} - -.alekpet_extras_node_speech_icon_playing::after { - content: "✖"; - position: absolute; - right: 3px; - color: var(--error-text); -} - -/* Settings */ -.alekpet_extras_node_speech_recognition_settings_props_label { - background: #646363; - color: #cdcbcb; - border-radius: 5px; - padding: 3px; - display: flex; - align-items: center; - justify-content: space-between; - flex-wrap: nowrap; -} - -.alekpet_extras_node_speech_recognition_settings_props_label:hover { - background: darkgrey; - color: white; -} - -.alekpet_extras_node_speech_recognition_settings_props_input { - max-width: 10px; - padding: 0; - margin: 0; - outline: 0; -} - -.alekpet_extras_node_speech_recognition_settings_button { - display: flex; - font-size: 0.6rem; -} - /* Style custom Confirm buttons */ .alekpet_confButtons { border-radius: 3px; diff --git a/ExtrasNode/js/extras_node.js b/ExtrasNode/js/extras_node.js index a169438..a4b8b5c 100644 --- a/ExtrasNode/js/extras_node.js +++ b/ExtrasNode/js/extras_node.js @@ -7,564 +7,17 @@ import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; import { ComfyWidgets } from "../../scripts/widgets.js"; -import { $el } from "../../scripts/ui.js"; -import { isValidStyle, comfyuiDesktopConfirm } from "./utils.js"; import { addStylesheet } from "../../scripts/utils.js"; -import { - SpeechWidget, - makeColorWidget, - createPreiviewSize, - speechRect, - SpeechSynthesis, - checkboxLSCheckedByKey, -} from "./lib/extrasnode/extras_node_widgets.js"; -import { RecognationSpeechDialog } from "./lib/extrasnode/extras_node_dialogs.js"; +import { makeColorWidget } from "./lib/extrasnode/extras_node_widgets.js"; -const convertIdClass = (text) => text.replaceAll(".", "_"); const idExt = "alekpet.ExtrasNode"; -// LocalStorage settings -const PreviewImageSizeLS = localStorage.getItem( - `Comfy.Settings.${idExt}.PreviewImage` -); -const PreviewImageColorTextLS = localStorage.getItem( - `Comfy.Settings.${idExt}.PreviewImageColorText` -); -const PreviewImageColorBgLS = localStorage.getItem( - `Comfy.Settings.${idExt}.PreviewImageColorBg` -); - -// Recognation & speech -const SpeechAndRecognationSpeechLS = localStorage.getItem( - `Comfy.Settings.${idExt}.SpeechAndRecognationSpeech` -); -// end - Recognation & speech - -// Preview image, video and audio select list combo -const PreviewImageVideoComboLS = localStorage.getItem( - `Comfy.Settings.${idExt}.PreviewImageVideoCombo` -); - -// Settings set values from LS or default -let PreviewImageSize = PreviewImageSizeLS - ? JSON.parse(PreviewImageSizeLS) - : false, - PreviewImageColorText = - PreviewImageColorTextLS && PreviewImageColorTextLS.trim() !== "" - ? PreviewImageColorTextLS - : document.documentElement.style.getPropertyValue("--input-text") || - "#dddddd", - PreviewImageColorBg = PreviewImageColorBgLS ? PreviewImageColorBgLS : "", - // Speech & Recognition widget settings - SpeechAndRecognationSpeech = SpeechAndRecognationSpeechLS - ? JSON.parse(SpeechAndRecognationSpeechLS) - : true, - SpeechAndRecognationSpeechSaveAs = JSON.parse( - localStorage.getItem(`${idExt}.SpeechAndRecognationSpeechSaveAs`), - false - ), - // Preview image, video and audio select list combo - PreviewImageVideoCombo = PreviewImageVideoComboLS - ? JSON.parse(PreviewImageVideoComboLS) - : true; - -// Preview image, video and audio content loading function -const SUPPORTS_FORMAT = { - image: ["jpg", "jpeg", "bmp", "png", "gif", "tiff", "avif"], - video: ["mp4", "webm"], - audio: ["ogg", "wav", "mp3", "webm"], -}; - -const loadingContent = (src) => { - return new Promise((res, rej) => { - let ext = src.slice(src.lastIndexOf(".") + 1).toLowerCase(); - ext = /\w+/.exec(ext)[0]; - - if (SUPPORTS_FORMAT.image.includes(ext)) { - const img = new Image(); - img.crossOrigin = ""; - img.onload = (e) => res({ raw: img, type: "image", src }); - img.onerror = (err) => rej(err); - img.src = src; - } - - if (SUPPORTS_FORMAT.video.includes(ext)) { - const video = document.querySelector(".preview_vid"); - video.onerror = (err) => rej(err); - - video.addEventListener("canplay", (e) => { - if (video.videoWidth !== 0) { - res({ raw: video, type: "video", src }); - } - }); - video.src = src; - } - - if (SUPPORTS_FORMAT.audio.includes(ext)) { - const audio = document.querySelector(".preview_audio"); - audio.onerror = (err) => rej(err); - - audio.addEventListener("canplaythrough", (e) => { - res({ raw: audio, type: "audio", src }); - }); - audio.src = src; - } - }); -}; - -function drawVid(raw, canvas, ctx, type) { - ctx.drawImage(raw, 0, 0, canvas.width, canvas.height); - - canvas.requanim = requestAnimationFrame( - drawVid.bind(this, raw, canvas, ctx, type) - ); -} -// - // Register Extension app.registerExtension({ name: idExt, init() { addStylesheet("css/extrasnode/extras_node_styles.css", import.meta.url); - - // -- Preview image, audio, video -- - - // Close when click on item inside context menu - function closePreviewContextClick() { - if (this.root?.preview_content_combo) { - this.root.preview_content_combo.remove(); - this.root.preview_content_combo = null; - } - } - - // Add preview element - function addPreviewModule(params, element) { - if (!PreviewImageVideoCombo) return; - - const [name, value, options] = params; - - if (value instanceof Object) return; - - if (!this.root?.preview_content_combo) { - const preview_content_combo = $el( - "div.preview_content_combo", - { - style: { - position: "absolute", - maxWidth: "160px", - zIndex: 9999999, - }, - parent: document.body, - }, - [ - $el("canvas.preview_img", { - style: { width: "100%" }, - }), - $el("video.preview_vid", { - style: { opacity: 0, width: 0, height: 0 }, - crossOrigin: "anonymous", - autoplay: false, - muted: false, - preload: "auto", - }), - $el("audio.preview_audio", { - style: { opacity: 0, position: "absolute", left: 0 }, - crossOrigin: "anonymous", - autoplay: false, - muted: false, - preload: "auto", - controls: true, - }), - ] - ); - - this.root.preview_content_combo = preview_content_combo; - } - - LiteGraph.pointerListenerAdd(element, "enter", (e) => { - if (element?.dataset?.value) { - const body_rect = document.body.getBoundingClientRect(); - const root_rect = this.root.getBoundingClientRect(); - const { x, y } = element.getBoundingClientRect(); - //const preview_rect = this.root.preview_content_combo.getBoundingClientRect(); - - //const scale = app.graph.extra.ds.scale; - - const canvas = this.root.preview_content_combo.children[0]; - const ctx = canvas.getContext("2d"); - - loadingContent( - api.apiURL( - `/view?filename=${encodeURIComponent( - element.dataset.value - )}&type=input` - ) - ).then(({ raw, type, src }) => { - this.root.preview_content_combo.style.maxWidth = - type === "audio" ? "300px" : "160px"; - - const previewWidth = parseInt( - this.root.preview_content_combo.style.maxWidth - ); - - // if (scale >= 1) { - // this.root.preview_content_combo.style.top = `${ - // (y - root_rect.top) / scale - // }px`; - // } else { - // this.root.preview_content_combo.style.top = `${ - // y - root_rect.top - // }px`; - // } - - this.root.preview_content_combo.style.top = `${y}px`; - - if ( - body_rect.width && - root_rect.right + previewWidth > body_rect.width - ) { - this.root.preview_content_combo.style.left = `${ - root_rect.left - previewWidth - 10 - }px`; - } else { - this.root.preview_content_combo.style.left = `${ - root_rect.left + root_rect.width + 10 - }px`; - } - - ctx.clearRect(0, 0, canvas.width, canvas.height); - const video = document.querySelector(".preview_vid"); - const audio = document.querySelector(".preview_audio"); - - if (type === "image") { - canvas.width = raw.naturalWidth; - canvas.height = raw.naturalHeight; - if (!video.paused) { - video.pause(); - video.currentTime = 0; - } - cancelAnimationFrame(canvas.requanim); - } else if (type === "video") { - canvas.width = raw.videoWidth; - canvas.height = raw.videoHeight; - video.play(); - } else if (type === "audio") { - Object.assign(audio.style, { - opacity: 1, - display: "", - }); - audio.play(); - } - - if (type === "video" || type === "image") { - Object.assign(audio.style, { - opacity: 0, - display: "none", - }); - !audio.paused && audio.pause(); - ctx.drawImage(raw, 0, 0, canvas.width, canvas.height); - } - - if (type === "video") - canvas.requanim = requestAnimationFrame( - drawVid.bind(this, raw, canvas, ctx, type) - ); - }); - } - }); - - LiteGraph.pointerListenerAdd(element, "leave", (e) => { - if (element?.dataset?.value) { - const canvas = this.root.preview_content_combo.children[0]; - const video = document.querySelector(".preview_vid"); - if (!video.paused) { - video.pause(); - video.currentTime = 0; - } - cancelAnimationFrame(canvas.requanim); - } - }); - } - - // AddItem - const addItem = ContextMenu.prototype.addItem; - ContextMenu.prototype.addItem = function () { - const element = addItem?.apply(this, arguments); - - addPreviewModule.call(this, arguments, element); - }; - - // CloseItem - const closeItem = ContextMenu.prototype.close; - ContextMenu.prototype.close = function () { - closeItem?.apply(this, arguments); - - closePreviewContextClick.call(this); - }; - - // Old verison comfyui, support context menu LiteGraph path... - // AddItem - const addItemLitegraph = LiteGraph.ContextMenu.prototype.addItem; - LiteGraph.ContextMenu.prototype.addItem = function () { - const element = addItemLitegraph?.apply(this, arguments); - - addPreviewModule.call(this, arguments, element); - }; - - // CloseItem - const closeItemLitegraph = LiteGraph.ContextMenu.prototype.close; - LiteGraph.ContextMenu.prototype.close = function () { - closeItemLitegraph?.apply(this, arguments); - - closePreviewContextClick.call(this); - }; - - const originalCloseAllContextMenus = LiteGraph.closeAllContextMenus; - LiteGraph.closeAllContextMenus = function () { - originalCloseAllContextMenus?.apply(this, arguments); - - const previewCombo = document.querySelector(".preview_content_combo"); - if (previewCombo) { - previewCombo.remove(); - } - }; - // -- end - Preview image, audio, video -- - - // PreviewImage settings ui - app.ui.settings.addSetting({ - id: `${idExt}.PreviewImage`, - name: "🔸 Preview Image", - defaultValue: true, - type: (name, sett, val) => { - const newUI = document.querySelector(".p-dialog-header"); - return $el("tr", [ - !newUI - ? $el("td", [ - $el("label", { - textContent: name, - for: convertIdClass(`${idExt}.PreviewImage_size_checkbox`), - }), - ]) - : "", - $el("td", [ - $el( - "label", - { - style: { display: "block" }, - textContent: "Display image size: ", - for: convertIdClass(`${idExt}.PreviewImage_size_checkbox`), - }, - [ - $el("input", { - id: convertIdClass(`${idExt}.PreviewImage_size_checkbox`), - type: "checkbox", - checked: val, - onchange: (e) => { - const checked = !!e.target.checked; - PreviewImageSize = checked; - - localStorage.setItem( - `Comfy.Settings.${idExt}.PreviewImage`, - checked - ); - sett(checked); - }, - }), - ] - ), - $el( - "label", - { - textContent: "Color text: ", - style: { display: "block" }, - }, - [ - $el("input", { - id: convertIdClass(`${idExt}.PreviewImage_color_input`), - type: "color", - value: PreviewImageColorText, - onchange: (e) => { - const colorVal = - e.target.value || - document.documentElement.style.getPropertyValue( - "--input-text" - ) || - "#dddddd"; - PreviewImageColorText = colorVal; - localStorage.setItem( - `Comfy.Settings.${idExt}.PreviewImageColorText`, - PreviewImageColorText - ); - }, - }), - ] - ), - $el( - "label", - { - textContent: "Color background: ", - style: { display: "block" }, - }, - [ - $el("input", { - id: convertIdClass( - `${idExt}.PreviewImage_color_background_input` - ), - type: "color", - value: PreviewImageColorBg, - onchange: (e) => { - const colorVal = e.target.value || ""; - PreviewImageColorBg = colorVal; - localStorage.setItem( - `Comfy.Settings.${idExt}.PreviewImageColorBg`, - PreviewImageColorBg - ); - }, - }), - ] - ), - $el("button", { - textContent: "Reset colors", - onclick: () => { - PreviewImageColorText = isValidStyle( - "color", - document.documentElement.style.getPropertyValue( - "--input-text" - ) || "#ddd" - ).color_hex; - PreviewImageColorBg = ""; - - document.querySelector( - "#alekpet_ExtrasNode_PreviewImage_color_input" - ).value = PreviewImageColorText; - document.querySelector( - "#alekpet_ExtrasNode_PreviewImage_color_background_input" - ).value = PreviewImageColorBg; - - localStorage.setItem( - `Comfy.Settings.${idExt}.PreviewImageColorText`, - PreviewImageColorText - ); - localStorage.setItem( - `Comfy.Settings.${idExt}.PreviewImageColorBg`, - PreviewImageColorBg - ); - }, - style: { - display: "block", - }, - }), - ]), - ]); - }, - }); - - // Speech & Recognition speech settings ui - app.ui.settings.addSetting({ - id: `${idExt}.SpeechAndRecognationSpeech`, - name: "🔸 Speak text & Recognition speech", - defaultValue: true, - type: (name, sett, val) => { - const newUI = document.querySelector(".p-dialog-header"); - return $el("tr", [ - !newUI - ? $el("td", [ - $el("label", { - textContent: name, - for: convertIdClass( - `${idExt}.SpeechAndRecognationSpeech_show` - ), - }), - ]) - : "", - $el("td", [ - $el( - "label", - { - style: { display: "flex", alignItems: "center" }, - textContent: "Enabled: ", - for: convertIdClass(`${idExt}.SpeechAndRecognationSpeech_show`), - }, - [ - $el("input", { - id: convertIdClass( - `${idExt}.SpeechAndRecognationSpeech_show` - ), - type: "checkbox", - checked: val, - onchange: async (e) => { - const checked = !!e.target.checked; - SpeechAndRecognationSpeech = checked; - localStorage.setItem( - `Comfy.Settings.${idExt}.SpeechAndRecognationSpeech`, - checked - ); - if (await comfyuiDesktopConfirm("Reload page?")) { - location.reload(); - } - sett(checked); - }, - }), - $el("span", { - textContent: "(Then reload the page)", - style: { fontSize: "0.6rem", color: "yellow" }, - }), - ] - ), - $el( - "div", - { - style: { - display: "flex", - gap: "5px", - margin: "5px 0", - }, - title: "Show modal window when saving recorded audio.", - }, - [ - $el("span", { textContent: "Output Save as?" }), - $el("input", { - type: "checkbox", - checked: SpeechAndRecognationSpeechSaveAs, - onchange: (e) => { - localStorage.setItem( - `${idExt}.SpeechAndRecognationSpeechSaveAs`, - !!e.target.checked - ); - SpeechAndRecognationSpeechSaveAs = !!e.target.checked; - checkboxLSCheckedByKey( - `${idExt}.SpeechAndRecognationSpeechSaveAs`, - ".alekpet_extras_node_recognition_saveAs" - ); - }, - }), - ] - ), - $el("button", { - textContent: "Speech settings", - onclick: () => { - new RecognationSpeechDialog().show(); - }, - style: { - display: "block", - }, - }), - ]), - ]); - }, - }); - - // Preview image, video and audio settings ui - app.ui.settings.addSetting({ - id: `${idExt}.PreviewImageVideoCombo`, - name: "🔸 Preview images and videos in the list", - defaultValue: true, - type: "boolean", - onChange(value) { - PreviewImageVideoCombo = value; - }, - }); }, async beforeRegisterNodeDef(nodeType, nodeData, app) { // --- Preview Text Node @@ -698,172 +151,10 @@ app.registerExtension({ }; break; } - case "PreviewImage": { - // Node Created - const onNodeCreated = nodeType.prototype.onNodeCreated; - nodeType.prototype.onNodeCreated = function () { - const ret = onNodeCreated - ? onNodeCreated.apply(this, arguments) - : undefined; - - if (PreviewImageSize) - createPreiviewSize(this, nodeData.name, { - color: PreviewImageColorText, - }); - - return ret; - }; - - const onExecuted = nodeType.prototype.onExecuted; - nodeType.prototype.onExecuted = function ({ images }) { - onExecuted?.apply(this, arguments); - - if (PreviewImageSize !== undefined) { - let widgetRes = this?.widgets?.find( - (w) => w.type === "show_resolution" - ); - - if (!PreviewImageSize) { - if (widgetRes) { - for (const w of this.widgets) { - if (w.type === "show_resolution") { - w?.onRemove(); - } - } - this.widgets.length = 0; - this.onResize(this.size); - } - } else { - if (!widgetRes) { - widgetRes = createPreiviewSize(this, nodeData.name, { - color: PreviewImageColorText, - }); - this.onResize(this.size); - } else { - widgetRes.element.style.color = PreviewImageColorText; - } - - if (images.length) { - const image = new Image(); - image.onload = () => { - widgetRes.value = `
Width: ${image.naturalWidth}, Height: ${ - image.naturalHeight - }
`; - }; - image.src = api.apiURL( - "/view?" + - new URLSearchParams(images[0]).toString() + - app.getPreviewFormatParam() + - app.getRandParam() - ); - } - } - } - }; - - break; - } default: { break; } } - - // -- Speech & Recognition speech widget - // If ui settings is true and SpeechSynthesis or speechRecognition is not undefined - if (SpeechAndRecognationSpeech && (speechRect || SpeechSynthesis)) { - let nodeIsMultiString = false; - let outputNode = false; - - if (nodeData?.input && nodeData?.input?.required) { - for (const inp of Object.keys(nodeData.input.required)) { - if (nodeData.input.required[inp][1]?.multiline) { - const type = nodeData.input.required[inp][0]; - - if (["STRING"].includes(type)) { - nodeIsMultiString = true; - break; - } - } - } - } - - if (nodeData?.output) { - for (const out of nodeData.output) { - if (["STRING"].includes(out)) { - nodeIsMultiString = true; - outputNode = true; - break; - } - } - } - - if (nodeIsMultiString) { - // Node Created - const onNodeCreated = nodeType.prototype.onNodeCreated; - nodeType.prototype.onNodeCreated = async function () { - const ret = onNodeCreated - ? onNodeCreated.apply(this, arguments) - : undefined; - - // Find all widget type customtext - const widgetsTextMulti = this?.widgets?.filter((w) => - !outputNode - ? ["customtext", "converted-widget"].includes(w.type) - : ["customtext"].includes(w.type) - ); - const isIncludesSpeech = this?.widgets?.some( - (w) => w.type === "speak_and_recognation_type" - ); - - await new Promise((res) => - setTimeout(() => { - res(); - }, 16 * this.widgets?.length ?? 1) - ); - - if (!isIncludesSpeech && widgetsTextMulti?.length) { - widgetsTextMulti.forEach(async (w) => { - this.addCustomWidget( - SpeechWidget(this, "speak_and_recognation", [false, true], w) - ); - }); - } - - return ret; - }; - - // onConfigure - const onConfigure = nodeType.prototype.onConfigure; - nodeType.prototype.onConfigure = async function (w) { - onConfigure?.apply(this, arguments); - if (w?.widgets_values?.length) { - await new Promise((res) => - setTimeout(() => { - res(); - }, 16 * this.widgets?.length ?? 1) - ); - - const ids_speech_clear = this.widgets.reduce(function ( - arr, - el, - idx - ) { - if (el.type === "speak_and_recognation_type") arr.push(idx); - return arr; - }, - []); - - for (const id of ids_speech_clear) - this?.widgets[id]?.callback(w.widgets_values[id]); - } - }; - } - } - // -- end - Speech & Recognition speech widget }, }); diff --git a/ExtrasNode/js/extras_preview_image_size.js b/ExtrasNode/js/extras_preview_image_size.js new file mode 100644 index 0000000..654f8db --- /dev/null +++ b/ExtrasNode/js/extras_preview_image_size.js @@ -0,0 +1,241 @@ +/* + * Title: Extras extension - Preview Image Size + * Author: AlekPet + * Github: https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ExtrasNode + */ + +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; +import { $el } from "../../scripts/ui.js"; +import { isValidStyle } from "./utils.js"; +import { createPreiviewSize } from "./lib/extrasnode/extras_node_widgets.js"; + +const convertIdClass = (text) => text.replaceAll(".", "_"); +const idExt = "alekpet.ExtrasNode"; + +// LocalStorage settings +const PreviewImageSizeLS = localStorage.getItem( + `Comfy.Settings.${idExt}.PreviewImage` +); +const PreviewImageColorTextLS = localStorage.getItem( + `Comfy.Settings.${idExt}.PreviewImageColorText` +); +const PreviewImageColorBgLS = localStorage.getItem( + `Comfy.Settings.${idExt}.PreviewImageColorBg` +); + +// Settings set values from LS or default +let PreviewImageSize = PreviewImageSizeLS + ? JSON.parse(PreviewImageSizeLS) + : false, + PreviewImageColorText = + PreviewImageColorTextLS && PreviewImageColorTextLS.trim() !== "" + ? PreviewImageColorTextLS + : document.documentElement.style.getPropertyValue("--input-text") || + "#dddddd", + PreviewImageColorBg = PreviewImageColorBgLS ? PreviewImageColorBgLS : ""; + +// -- Extension: PreviewImageSize -- +app.registerExtension({ + name: `${idExt}.PreviewImageSize`, + + init() { + // PreviewImage settings ui + app.ui.settings.addSetting({ + id: `${idExt}.PreviewImage`, + name: "🔸 Preview Image Size", + defaultValue: true, + type: (name, sett, val) => { + const newUI = document.querySelector(".p-dialog-header"); + return $el("tr", [ + !newUI + ? $el("td", [ + $el("label", { + textContent: name, + for: convertIdClass(`${idExt}.PreviewImage_size_checkbox`), + }), + ]) + : "", + $el("td", [ + $el( + "label", + { + style: { display: "block" }, + textContent: "Display image size: ", + for: convertIdClass(`${idExt}.PreviewImage_size_checkbox`), + }, + [ + $el("input", { + id: convertIdClass(`${idExt}.PreviewImage_size_checkbox`), + type: "checkbox", + checked: val, + onchange: (e) => { + const checked = !!e.target.checked; + PreviewImageSize = checked; + + localStorage.setItem( + `Comfy.Settings.${idExt}.PreviewImage`, + checked + ); + sett(checked); + }, + }), + ] + ), + $el( + "label", + { + textContent: "Color text: ", + style: { display: "block" }, + }, + [ + $el("input", { + id: convertIdClass(`${idExt}.PreviewImage_color_input`), + type: "color", + value: PreviewImageColorText, + onchange: (e) => { + const colorVal = + e.target.value || + document.documentElement.style.getPropertyValue( + "--input-text" + ) || + "#dddddd"; + PreviewImageColorText = colorVal; + localStorage.setItem( + `Comfy.Settings.${idExt}.PreviewImageColorText`, + PreviewImageColorText + ); + }, + }), + ] + ), + $el( + "label", + { + textContent: "Color background: ", + style: { display: "block" }, + }, + [ + $el("input", { + id: convertIdClass( + `${idExt}.PreviewImage_color_background_input` + ), + type: "color", + value: PreviewImageColorBg, + onchange: (e) => { + const colorVal = e.target.value || ""; + PreviewImageColorBg = colorVal; + localStorage.setItem( + `Comfy.Settings.${idExt}.PreviewImageColorBg`, + PreviewImageColorBg + ); + }, + }), + ] + ), + $el("button", { + textContent: "Reset colors", + onclick: () => { + PreviewImageColorText = isValidStyle( + "color", + document.documentElement.style.getPropertyValue( + "--input-text" + ) || "#ddd" + ).color_hex; + PreviewImageColorBg = ""; + + document.querySelector( + "#alekpet_ExtrasNode_PreviewImage_color_input" + ).value = PreviewImageColorText; + document.querySelector( + "#alekpet_ExtrasNode_PreviewImage_color_background_input" + ).value = PreviewImageColorBg; + + localStorage.setItem( + `Comfy.Settings.${idExt}.PreviewImageColorText`, + PreviewImageColorText + ); + localStorage.setItem( + `Comfy.Settings.${idExt}.PreviewImageColorBg`, + PreviewImageColorBg + ); + }, + style: { + display: "block", + }, + }), + ]), + ]); + }, + }); + }, + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === "PreviewImage") { + // Node Created + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + const ret = onNodeCreated + ? onNodeCreated.apply(this, arguments) + : undefined; + + if (PreviewImageSize) + createPreiviewSize(this, nodeData.name, { + color: PreviewImageColorText, + }); + + return ret; + }; + + const onExecuted = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function ({ images }) { + onExecuted?.apply(this, arguments); + + if (PreviewImageSize !== undefined) { + let widgetRes = this?.widgets?.find( + (w) => w.type === "show_resolution" + ); + + if (!PreviewImageSize) { + if (widgetRes) { + for (const w of this.widgets) { + if (w.type === "show_resolution") { + w?.onRemove(); + } + } + this.widgets.length = 0; + this.onResize(this.size); + } + } else { + if (!widgetRes) { + widgetRes = createPreiviewSize(this, nodeData.name, { + color: PreviewImageColorText, + }); + this.onResize(this.size); + } else { + widgetRes.element.style.color = PreviewImageColorText; + } + + if (images.length) { + const image = new Image(); + image.onload = () => { + widgetRes.value = `
Width: ${image.naturalWidth}, Height: ${ + image.naturalHeight + }
`; + }; + image.src = api.apiURL( + "/view?" + + new URLSearchParams(images[0]).toString() + + app.getPreviewFormatParam() + + app.getRandParam() + ); + } + } + } + }; + } + }, +}); diff --git a/ExtrasNode/js/extras_preview_media.js b/ExtrasNode/js/extras_preview_media.js new file mode 100644 index 0000000..df3a264 --- /dev/null +++ b/ExtrasNode/js/extras_preview_media.js @@ -0,0 +1,294 @@ +/* + * Title: Extras extension - Preview Media (image, audio, video) + * Author: AlekPet + * Github: https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ExtrasNode + */ + +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; +import { $el } from "../../scripts/ui.js"; + +const idExt = "alekpet.ExtrasNode"; + +// LocalStorage settings - Preview image, video and audio select list combo +const PreviewImageVideoComboLS = localStorage.getItem( + `Comfy.Settings.${idExt}.PreviewImageVideoCombo` +); + +// Settings set values from LS or default - Preview image, video and audio select list combo +let PreviewImageVideoCombo = PreviewImageVideoComboLS + ? JSON.parse(PreviewImageVideoComboLS) + : true; + +// Preview image, video and audio content loading function +const SUPPORTS_FORMAT = { + image: ["jpg", "jpeg", "bmp", "png", "gif", "tiff", "avif"], + video: ["mp4", "webm"], + audio: ["ogg", "wav", "mp3", "webm"], +}; + +const loadingContent = (src) => { + return new Promise((res, rej) => { + let ext = src.slice(src.lastIndexOf(".") + 1).toLowerCase(); + ext = /\w+/.exec(ext)[0]; + + if (SUPPORTS_FORMAT.image.includes(ext)) { + const img = new Image(); + img.crossOrigin = ""; + img.onload = (e) => res({ raw: img, type: "image", src }); + img.onerror = (err) => rej(err); + img.src = src; + } + + if (SUPPORTS_FORMAT.video.includes(ext)) { + const video = document.querySelector(".preview_vid"); + video.onerror = (err) => rej(err); + + video.addEventListener("canplay", (e) => { + if (video.videoWidth !== 0) { + res({ raw: video, type: "video", src }); + } + }); + video.src = src; + } + + if (SUPPORTS_FORMAT.audio.includes(ext)) { + const audio = document.querySelector(".preview_audio"); + audio.onerror = (err) => rej(err); + + audio.addEventListener("canplaythrough", (e) => { + res({ raw: audio, type: "audio", src }); + }); + audio.src = src; + } + }); +}; + +function drawVid(raw, canvas, ctx, type) { + ctx.drawImage(raw, 0, 0, canvas.width, canvas.height); + + canvas.requanim = requestAnimationFrame( + drawVid.bind(this, raw, canvas, ctx, type) + ); +} +// + +// -- Extension: Preview image, audio, video -- +app.registerExtension({ + name: `${idExt}.PreviewMedia`, + init() { + // Preview image, video and audio settings ui + app.ui.settings.addSetting({ + id: `${idExt}.PreviewImageVideoCombo`, + name: "🔸 Preview images and videos in the list", + defaultValue: true, + type: "boolean", + onChange(value) { + PreviewImageVideoCombo = value; + }, + }); + + // -- Preview image, audio, video -- + + // Close when click on item inside context menu + function closePreviewContextClick() { + if (this.root?.preview_content_combo) { + this.root.preview_content_combo.remove(); + this.root.preview_content_combo = null; + } + } + + // Add preview element + function addPreviewModule(params, element) { + if (!PreviewImageVideoCombo) return; + + const [name, value, options] = params; + + if (value instanceof Object) return; + + if (!this.root?.preview_content_combo) { + const preview_content_combo = $el( + "div.preview_content_combo", + { + style: { + position: "absolute", + maxWidth: "160px", + zIndex: 9999999, + }, + parent: document.body, + }, + [ + $el("canvas.preview_img", { + style: { width: "100%" }, + }), + $el("video.preview_vid", { + style: { opacity: 0, width: 0, height: 0 }, + crossOrigin: "anonymous", + autoplay: false, + muted: false, + preload: "auto", + }), + $el("audio.preview_audio", { + style: { opacity: 0, position: "absolute", left: 0 }, + crossOrigin: "anonymous", + autoplay: false, + muted: false, + preload: "auto", + controls: true, + }), + ] + ); + + this.root.preview_content_combo = preview_content_combo; + } + + LiteGraph.pointerListenerAdd(element, "enter", (e) => { + if (element?.dataset?.value) { + const body_rect = document.body.getBoundingClientRect(); + const root_rect = this.root.getBoundingClientRect(); + const { x, y } = element.getBoundingClientRect(); + //const preview_rect = this.root.preview_content_combo.getBoundingClientRect(); + + //const scale = app.graph.extra.ds.scale; + + const canvas = this.root.preview_content_combo.children[0]; + const ctx = canvas.getContext("2d"); + + loadingContent( + api.apiURL( + `/view?filename=${encodeURIComponent( + element.dataset.value + )}&type=input` + ) + ).then(({ raw, type, src }) => { + this.root.preview_content_combo.style.maxWidth = + type === "audio" ? "300px" : "160px"; + + const previewWidth = parseInt( + this.root.preview_content_combo.style.maxWidth + ); + + // if (scale >= 1) { + // this.root.preview_content_combo.style.top = `${ + // (y - root_rect.top) / scale + // }px`; + // } else { + // this.root.preview_content_combo.style.top = `${ + // y - root_rect.top + // }px`; + // } + + this.root.preview_content_combo.style.top = `${y}px`; + + if ( + body_rect.width && + root_rect.right + previewWidth > body_rect.width + ) { + this.root.preview_content_combo.style.left = `${ + root_rect.left - previewWidth - 10 + }px`; + } else { + this.root.preview_content_combo.style.left = `${ + root_rect.left + root_rect.width + 10 + }px`; + } + + ctx.clearRect(0, 0, canvas.width, canvas.height); + const video = document.querySelector(".preview_vid"); + const audio = document.querySelector(".preview_audio"); + + if (type === "image") { + canvas.width = raw.naturalWidth; + canvas.height = raw.naturalHeight; + if (!video.paused) { + video.pause(); + video.currentTime = 0; + } + cancelAnimationFrame(canvas.requanim); + } else if (type === "video") { + canvas.width = raw.videoWidth; + canvas.height = raw.videoHeight; + video.play(); + } else if (type === "audio") { + Object.assign(audio.style, { + opacity: 1, + display: "", + }); + audio.play(); + } + + if (type === "video" || type === "image") { + Object.assign(audio.style, { + opacity: 0, + display: "none", + }); + !audio.paused && audio.pause(); + ctx.drawImage(raw, 0, 0, canvas.width, canvas.height); + } + + if (type === "video") + canvas.requanim = requestAnimationFrame( + drawVid.bind(this, raw, canvas, ctx, type) + ); + }); + } + }); + + LiteGraph.pointerListenerAdd(element, "leave", (e) => { + if (element?.dataset?.value) { + const canvas = this.root.preview_content_combo.children[0]; + const video = document.querySelector(".preview_vid"); + if (!video.paused) { + video.pause(); + video.currentTime = 0; + } + cancelAnimationFrame(canvas.requanim); + } + }); + } + + // AddItem + const addItem = ContextMenu.prototype.addItem; + ContextMenu.prototype.addItem = function () { + const element = addItem?.apply(this, arguments); + + addPreviewModule.call(this, arguments, element); + }; + + // CloseItem + const closeItem = ContextMenu.prototype.close; + ContextMenu.prototype.close = function () { + closeItem?.apply(this, arguments); + + closePreviewContextClick.call(this); + }; + + // Old verison comfyui, support context menu LiteGraph path... + // AddItem + const addItemLitegraph = LiteGraph.ContextMenu.prototype.addItem; + LiteGraph.ContextMenu.prototype.addItem = function () { + const element = addItemLitegraph?.apply(this, arguments); + + addPreviewModule.call(this, arguments, element); + }; + + // CloseItem + const closeItemLitegraph = LiteGraph.ContextMenu.prototype.close; + LiteGraph.ContextMenu.prototype.close = function () { + closeItemLitegraph?.apply(this, arguments); + + closePreviewContextClick.call(this); + }; + + const originalCloseAllContextMenus = LiteGraph.closeAllContextMenus; + LiteGraph.closeAllContextMenus = function () { + originalCloseAllContextMenus?.apply(this, arguments); + + const previewCombo = document.querySelector(".preview_content_combo"); + if (previewCombo) { + previewCombo.remove(); + } + }; + // -- end - Preview image, audio, video -- + }, +}); diff --git a/ExtrasNode/js/extras_speech_and_recognition.js b/ExtrasNode/js/extras_speech_and_recognition.js new file mode 100644 index 0000000..ecd1a0b --- /dev/null +++ b/ExtrasNode/js/extras_speech_and_recognition.js @@ -0,0 +1,226 @@ +/* + * Title: Extras extension - Speech And Recognition + * Author: AlekPet + * Github: https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ExtrasNode + */ +import { app } from "../../scripts/app.js"; +import { $el } from "../../scripts/ui.js"; +import { comfyuiDesktopConfirm } from "./utils.js"; +import { addStylesheet } from "../../scripts/utils.js"; +import { + SpeechWidget, + speechRect, + SpeechSynthesis, + checkboxLSCheckedByKey, +} from "./lib/extrasnode/extras_speech_and_recognation_widget.js"; +import { RecognationSpeechDialog } from "./lib/extrasnode/extras_speech_and_recognation_dialog.js"; + +const convertIdClass = (text) => text.replaceAll(".", "_"); +const idExt = "alekpet.ExtrasNode"; + +// Recognation & speech localStorage settings +const SpeechAndRecognationSpeechLS = localStorage.getItem( + `Comfy.Settings.${idExt}.SpeechAndRecognationSpeech` +); + +// Settings set values from LS or default for Speech & Recognition widget settings +let SpeechAndRecognationSpeech = SpeechAndRecognationSpeechLS + ? JSON.parse(SpeechAndRecognationSpeechLS) + : true, + SpeechAndRecognationSpeechSaveAs = JSON.parse( + localStorage.getItem(`${idExt}.SpeechAndRecognationSpeechSaveAs`), + false + ); + +// -- Extension: Speak text & Recognition speech -- +app.registerExtension({ + name: `${idExt}.SpeechAndRecognationSpeech`, + + init() { + addStylesheet( + "css/extrasnode/extras_node_speech_recognition_style.css", + import.meta.url + ); + + // Speech & Recognition speech settings ui + app.ui.settings.addSetting({ + id: `${idExt}.SpeechAndRecognationSpeech`, + name: "🔸 Speak text & Recognition speech", + defaultValue: true, + type: (name, sett, val) => { + const newUI = document.querySelector(".p-dialog-header"); + return $el("tr", [ + !newUI + ? $el("td", [ + $el("label", { + textContent: name, + for: convertIdClass( + `${idExt}.SpeechAndRecognationSpeech_show` + ), + }), + ]) + : "", + $el("td", [ + $el( + "label", + { + style: { display: "flex", alignItems: "center" }, + textContent: "Enabled: ", + for: convertIdClass(`${idExt}.SpeechAndRecognationSpeech_show`), + }, + [ + $el("input", { + id: convertIdClass( + `${idExt}.SpeechAndRecognationSpeech_show` + ), + type: "checkbox", + checked: val, + onchange: async (e) => { + const checked = !!e.target.checked; + SpeechAndRecognationSpeech = checked; + localStorage.setItem( + `Comfy.Settings.${idExt}.SpeechAndRecognationSpeech`, + checked + ); + if (await comfyuiDesktopConfirm("Reload page?")) { + location.reload(); + } + sett(checked); + }, + }), + $el("span", { + textContent: "(Then reload the page)", + style: { fontSize: "0.6rem", color: "yellow" }, + }), + ] + ), + $el( + "div", + { + style: { + display: "flex", + gap: "5px", + margin: "5px 0", + }, + title: "Show modal window when saving recorded audio.", + }, + [ + $el("span", { textContent: "Output Save as?" }), + $el("input", { + type: "checkbox", + checked: SpeechAndRecognationSpeechSaveAs, + onchange: (e) => { + localStorage.setItem( + `${idExt}.SpeechAndRecognationSpeechSaveAs`, + !!e.target.checked + ); + SpeechAndRecognationSpeechSaveAs = !!e.target.checked; + checkboxLSCheckedByKey( + `${idExt}.SpeechAndRecognationSpeechSaveAs`, + ".alekpet_extras_node_recognition_saveAs" + ); + }, + }), + ] + ), + $el("button", { + textContent: "Speech settings", + onclick: () => { + new RecognationSpeechDialog().show(); + }, + style: { + display: "block", + }, + }), + ]), + ]); + }, + }); + }, + async beforeRegisterNodeDef(nodeType, nodeData, app) { + // -- Speech & Recognition speech widget + // If ui settings is true and SpeechSynthesis or speechRecognition is not undefined + if (SpeechAndRecognationSpeech && (speechRect || SpeechSynthesis)) { + // Node Created + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = async function () { + const ret = onNodeCreated + ? onNodeCreated.apply(this, arguments) + : undefined; + + let nodeIsMultiString = false; + + if (nodeData?.input && nodeData?.input?.required) { + for (const inp of Object.keys(nodeData.input.required)) { + if ( + nodeData.input.required[inp][1]?.multiline && + !nodeData.input.required[inp][1]?.forceInput + ) { + const type = nodeData.input.required[inp][0]; + + if (["STRING"].includes(type)) { + nodeIsMultiString = true; + break; + } + } + } + } + + if (nodeData?.output) { + for (const out of nodeData.output) { + const isElementTextArea = this?.widgets?.some( + (w) => + w?.element?.tagName === "TEXTAREA" || + w?.inputEl?.tagName === "TEXTAREA" + ); + + if (isElementTextArea && ["STRING"].includes(out)) { + nodeIsMultiString = true; + break; + } + } + } + + if (!nodeIsMultiString) return ret; + + // Find all widget type customtext + const widgetsTextMulti = this?.widgets?.filter((w) => + ["customtext", "converted-widget"].includes(w.type) + ); + + const isIncludesSpeech = this?.widgets?.some( + (w) => w.type === "speak_and_recognation_type" + ); + + if (!isIncludesSpeech && widgetsTextMulti?.length) { + widgetsTextMulti.forEach(async (w) => { + this.addCustomWidget( + SpeechWidget(this, "speak_and_recognation", [false, true], w) + ); + }); + } + + return ret; + }; + + // onConfigure + const onConfigure = nodeType.prototype.onConfigure; + nodeType.prototype.onConfigure = async function (w) { + onConfigure?.apply(this, arguments); + + if (w?.widgets_values?.length) { + const ids_speech_clear = this.widgets.reduce(function (arr, el, idx) { + if (el.type === "speak_and_recognation_type") arr.push(idx); + return arr; + }, []); + + if (!ids_speech_clear.length) return; + + for (const id of ids_speech_clear) + this?.widgets[id]?.callback(w.widgets_values[id]); + } + }; + } + // -- end - Speech & Recognition speech widget + }, +}); diff --git a/ExtrasNode/lib/extras_node_widgets.js b/ExtrasNode/lib/extras_node_widgets.js index d7f930e..92ce2fc 100644 --- a/ExtrasNode/lib/extras_node_widgets.js +++ b/ExtrasNode/lib/extras_node_widgets.js @@ -4,687 +4,12 @@ * Github: https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ExtrasNode */ -import { api } from "../../../../scripts/api.js"; -import { app } from "../../../../scripts/app.js"; import { $el } from "../../../../scripts/ui.js"; -import { - rgbToHex, - isValidStyle, - createWindowModal, - animateClick, - comfyuiDesktopAlert, -} from "../../utils.js"; -import { RecognationSpeechDialog } from "./extras_node_dialogs.js"; +import { rgbToHex, isValidStyle } from "../../utils.js"; const idExt = "alekpet.ExtrasNode"; const CONVERTED_TYPE = "converted-widget"; -/* ~~~ Speech & Recognition speech Widget ~~~ */ -const spRecognition = - window.SpeechRecognition || window.webkitSpeechRecognition; -const SpeechSynthesis = window.speechSynthesis; - -const regExpFileName = /[^\p{L}\d]+/gu; -const maxLenAudioFileName = 50; - -let speechRect; -let mediaRecorder = null; -let audioChunks = []; - -if (spRecognition) { - speechRect = new spRecognition(); - speechRect.elements = null; - speechRect.isRecognition = false; - speechRect.isRecognitionAbort = false; - speechRect.lastText = ""; - - speechRect.addEventListener("result", (event) => { - const results = event?.results[0][0]?.transcript; - if (results && speechRect?.elements?.length) { - const clear_text = speechRect.elements[0].querySelector( - ".alekpet_extras_node_recognition_clear" - ); - - if (clear_text.checked) { - speechRect.elements[1].value = results; - } else { - speechRect.elements[1].value += " " + results; - } - } - }); - - const resetSpeechRecognition = () => { - if (speechRect?.elements?.length) { - const info = speechRect.elements[0].querySelector( - ".alekpet_extras_node_info span" - ); - const icon_rec = speechRect.elements[0].querySelector( - ".alekpet_extras_node_recognition_icon" - ); - - speechRect.elements[0].querySelector( - ".alekpet_extras_node_recognition_aborted" - ).style.display = "none"; - - icon_rec.classList.remove("alekpet_extras_node_recognition_icon_active"); - setTimeout(() => (info.textContent = ""), 1000); - } - - setStylesAllElements(".alekpet_extras_node_recognition_icon", null, { - display: "inline-block", - }); - - if (mediaRecorder) { - speechRect.lastText = speechRect?.elements[1].value; - mediaRecorder.stop(); - } else { - speechRect.lastText = ""; - speechRect.isRecognitionAbort = false; - } - - mediaRecorder = null; - audioChunks = []; - speechRect.isRecognition = false; - - speechRect.elements = null; - }; - - speechRect.addEventListener("audiostart", (e) => { - speechRect.isRecognition = true; - }); - - speechRect.addEventListener("end", () => { - resetSpeechRecognition(); - speechRect.stop(); - }); - - speechRect.addEventListener("speechend", () => { - resetSpeechRecognition(); - speechRect.stop(); - }); - - speechRect.addEventListener("error", (event) => { - if (speechRect?.elements?.length) { - const info = speechRect.elements[0].querySelector( - ".alekpet_extras_node_info span" - ); - info.textContent = event.error; - } - resetSpeechRecognition(); - console.log(">> Error recognition: " + event.error); - }); -} else { - console.warn( - "Your browser does not support: SpeechRecognition.\nIf Firefox: go to about:config > media.webspeech.recognition.enable and media.webspeech.recognition.force_enable > set true" - ); -} - -if (!SpeechSynthesis) { - console.warn("Your browser does not support: speechSynthesis"); -} - -// Get position speech SpeechRecognition widget -function getPostition(ctx, w_width, y, n_height, wInput) { - const MARGIN = 10; - - const rect = ctx.canvas.getBoundingClientRect(); - const transform = new DOMMatrix() - .scaleSelf(rect.width / ctx.canvas.width, rect.height / ctx.canvas.height) - .multiplySelf(ctx.getTransform()) - .translateSelf(MARGIN, MARGIN); - const scale = new DOMMatrix().scaleSelf(transform.a, transform.d); - - return { - transformOrigin: "0 0", - transform: scale, - transform: transform, - left: `${transform.a * w_width - 70 * scale.a + rect.left}px`, - top: `${(wInput.last_y - 15) * scale.d + scale.f + rect.top}px`, - maxWidth: `${w_width - MARGIN * 2}px`, - maxHeight: `${n_height - MARGIN * 2}px`, - zIndex: wInput?.inputEl?.style?.zIndex - ? +wInput?.inputEl.style.zIndex + 1 - : 20, - }; -} - -function getVoiceAndSettings() { - const voices = speechSynthesis.getVoices(); - - const { voice, volume, pitch, rate } = - RecognationSpeechDialog.getSettingsRecSpeechLS(); - const voiceSelected = voices.filter((v) => v.name === voice); - - return { - voice: voiceSelected.length ? voiceSelected[0] : null, - volume, - pitch, - rate, - }; -} - -// Function return utterance -function speakSynthesisUtterance(text, options = {}) { - const utterance = new SpeechSynthesisUtterance(text); - - Object.assign(utterance, { ...options, ...getVoiceAndSettings() }); - return utterance; -} - -// Check premissions -async function checkPremissions( - device = { name: "microphone" }, - update = null -) { - return navigator.permissions - .query(device) - .then((result) => { - const state = result.state; - if (state == "granted") { - return { device, state, status: true }; - } else if (state == "prompt") { - return { device, state, status: false }; - } else if (state == "denied") { - return { device, state, status: false }; - } - result.onchange = update; - }) - .catch((e) => ({ device, state: "error", status: false })); -} - -// Check checkbox -function checkboxLSCheckedByKey(lsKey, selector, defaultVal = false) { - const currValue = JSON.parse(localStorage.getItem(lsKey, defaultVal)); - - Array.from(document.querySelectorAll(selector)).forEach( - (saveAs) => (saveAs.checked = currValue) - ); -} - -// Set styles -const setStylesAllElements = (selector, exclude = null, styles = {}) => { - let elements = Array.from(document.querySelectorAll(selector)); - if (exclude) elements = elements.filter((r) => r !== exclude); - elements = elements.map((r) => Object.assign(r.style, styles)); -}; - -function SpeechWidget(node, inputName, inputData, widgetsText) { - const widget = { - type: "speak_and_recognation_type", - name: inputName, - value: inputData, - size: [22, 1], - options: { hideOnZoom: true }, - text_element: widgetsText?.inputEl, - draw(ctx, node, widget_width, y, widget_height) { - const hidden = widgetsText?.element?.hidden || widgetsText.hidden; - - widget.element.style.display = - hidden || app.canvas.ds.scale < 0.6 ? "none" : "flex"; - if (hidden) { - widget.options.onHide?.(widget); - } - - if (hidden) { - return; - } - - Object.assign( - widget.element.style, - getPostition(ctx, widget_width, y, node.size[1], widgetsText) - ); - }, - computeSize(...args) { - return [22, 1]; - }, - async callback(v) { - if (widgetsText?.element?.hasAttribute("readonly")) return; - - widget.value = v ?? inputData ?? [false, true]; - - const checkboxSave = widget.element.querySelector( - ".alekpet_extras_node_recognition_save" - ); - const checkboxClear = widget.element.querySelector( - ".alekpet_extras_node_recognition_clear" - ); - - if (checkboxClear) checkboxClear.checked = widget.value[1] ?? false; - if (checkboxSave) checkboxSave.checked = widget.value[0] ?? false; - }, - onRemove() { - widget.element?.remove(); - }, - }; - - const buttons = []; - - if (speechRect && !widgetsText?.element?.hasAttribute("readonly")) { - buttons.push( - $el("div.alekpet_extras_node_recognition_icon_box", [ - $el("span.alekpet_extras_node_recognition_aborted", { - title: "Speech recognition abort", - textContent: "✖", - style: { display: "none" }, - onclick: async function (e) { - const info = widget.element.querySelector( - ".alekpet_extras_node_info span" - ); - - if (speechRect.isRecognition) { - e.currentTarget.style.display = "none"; - speechRect.isRecognitionAbort = true; - speechRect.abort(); - info.textContent = "aborted"; - this.classList.remove( - "alekpet_extras_node_recognition_icon_active" - ); - setTimeout(() => (info.textContent = ""), 2000); - } - }, - }), - $el("span.alekpet_extras_node_recognition_icon", { - title: "Speech recognition", - onclick: async function (e) { - const info = widget.element.querySelector( - ".alekpet_extras_node_info span" - ); - const abort = widget.element.querySelector( - ".alekpet_extras_node_recognition_aborted" - ); - const checkboxSave = widget.element.querySelector( - ".alekpet_extras_node_recognition_save" - ); - - // Hide other recognitions buttons - setStylesAllElements( - ".alekpet_extras_node_recognition_icon", - e.currentTarget, - { - display: "none", - } - ); - - // Recognition - if (speechRect.elements === null) { - // Record audio - if (checkboxSave.checked) { - try { - const stream = await navigator.mediaDevices.getUserMedia({ - audio: true, - }); - - mediaRecorder = new MediaRecorder(stream); - - mediaRecorder.ondataavailable = (event) => { - audioChunks.push(event.data); - }; - - mediaRecorder.onstop = async () => { - if (speechRect.isRecognitionAbort) { - speechRect.lastText = ""; - speechRect.isRecognitionAbort = false; - return; - } - - const saveAsWindow = JSON.parse( - localStorage.getItem( - `${idExt}.SpeechAndRecognationSpeechSaveAs`, - false - ) - ); - - // Filename - let nameFile = "recording.webm"; - if (speechRect?.lastText?.length) { - nameFile = `${speechRect.lastText - .slice(0, maxLenAudioFileName) - .replaceAll(regExpFileName, "_")}.webm`; - } - - // Get audio - const audioBlob = new Blob(audioChunks, { - type: "audio/webm", - }); - - if (!saveAsWindow) { - const body = new FormData(); - body.append("image", audioBlob, nameFile); - body.append("overwrite", "true"); - const resp = await api.fetchApi("/upload/image", { - method: "POST", - body, - }); - - if (resp.status !== 200) { - console.error( - "[ExtrasNode] Recording audio not saved!" - ); - return; - } - - console.log( - `[ExtrasNode] Recording audio "${nameFile}" saved successfully!` - ); - } else { - const audioUrl = URL.createObjectURL(audioBlob); - const linkDown = document.createElement("a"); - linkDown.href = audioUrl; - linkDown.download = nameFile; - linkDown.click(); - } - - speechRect.lastText = ""; - }; - } catch (err) { - comfyuiDesktopAlert( - `Device "Microphone" - ${err.message}!\n\nCheck device or allow access!` - ); - } - } - // end - Record audio - - speechRect.elements = [widget.element, widgetsText.inputEl]; - info.textContent = "recognition"; - this.classList.add("alekpet_extras_node_recognition_icon_active"); - speechRect.start(); - mediaRecorder && mediaRecorder.start(); - abort.style.display = "inline-block"; - } else { - speechRect.stop(); - abort.style.display = "none"; - info.textContent = "stoped"; - this.classList.remove( - "alekpet_extras_node_recognition_icon_active" - ); - setTimeout(() => (info.textContent = ""), 2000); - } - }, - }), - // Settings elements - $el("div.alekpet_extras_node_speech_recognition_settings", [ - $el( - "div.alekpet_extras_node_speech_recognition_settings_button.pi.pi-cog", - { - title: "Settings", - onclick: (e) => { - checkboxLSCheckedByKey( - `${idExt}.SpeechAndRecognationSpeechSaveAs`, - ".alekpet_extras_node_recognition_saveAs" - ); - animateClick(e.currentTarget.nextElementSibling); - }, - } - ), - createWindowModal({ - textTitle: null, - stylesClose: { - top: "-4px", - right: "4px", - width: "auto", - height: "auto", - padding: "2px", - fontSize: "0.4rem", - lineHeight: 1, - }, - stylesBox: { - background: "transparent", - border: 0, - padding: 0, - boxShadow: "none", - }, - stylesBody: { - display: "flex", - flexDirection: "column", - alignItems: "stretch", - gap: "3px", - textWrap: "wrap", - background: "rgb(131, 131, 131)", - color: "white", - padding: "2px", - marginTop: "2px", - borderRadius: "6px", - fontSize: "0.4rem", - minWidth: "85px", - }, - stylesWrapper: { - minWidth: "100px", - transform: "translate(0%, 0%)", - }, - textBody: [ - $el( - "label.alekpet_extras_node_speech_recognition_settings_props_label", - [ - $el( - "span.alekpet_extras_node_speech_recognition_settings_props_name", - { - textContent: "Recoding audio", - title: "Save in audio file after recognition", - } - ), - $el( - "input.alekpet_extras_node_speech_recognition_settings_props_input.alekpet_extras_node_speech_recognition_checkbox.alekpet_extras_node_recognition_save", - { - type: "checkbox", - checked: widget.value[0] ?? false, - onchange: (e) => - widget?.callback([!!e.target.checked, widget.value[1]]), - } - ), - ] - ), - $el( - "label.alekpet_extras_node_speech_recognition_settings_props_label", - [ - $el( - "span.alekpet_extras_node_speech_recognition_settings_props_name", - { - textContent: "Clear text", - title: "Clear text after recognition", - } - ), - $el( - "input.alekpet_extras_node_speech_recognition_settings_props_input.alekpet_extras_node_speech_recognition_checkbox.alekpet_extras_node_recognition_clear", - { - type: "checkbox", - checked: widget.value[1] ?? true, - onchange: (e) => - widget?.callback([widget.value[0], !!e.target.checked]), - } - ), - ] - ), - $el("hr", { style: { padding: 0, margin: 0 } }), - $el( - "label.alekpet_extras_node_speech_recognition_settings_props_label", - [ - $el( - "span.alekpet_extras_node_speech_recognition_settings_props_name", - { - textContent: "'Save as' window?", - title: "Show modal window when saving recorded audio.", - } - ), - $el( - "input.alekpet_extras_node_speech_recognition_settings_props_input.alekpet_extras_node_speech_recognition_checkbox.alekpet_extras_node_recognition_saveAs", - { - type: "checkbox", - checked: JSON.parse( - localStorage.getItem( - `${idExt}.SpeechAndRecognationSpeechSaveAs`, - false - ) - ), - onchange: (e) => { - const check = !!e.target.checked; - const settCheck = document.body.querySelector( - "[id$='.SpeechAndRecognationSpeech'] div input" - ); - settCheck && (settCheck.checked = check); - - localStorage.setItem( - `${idExt}.SpeechAndRecognationSpeechSaveAs`, - check - ); - - checkboxLSCheckedByKey( - `${idExt}.SpeechAndRecognationSpeechSaveAs`, - ".alekpet_extras_node_recognition_saveAs" - ); - }, - } - ), - ] - ), - ], - }), - ]), - // end -- Settings elements - ]) - ); - } - - if (SpeechSynthesis && speechSynthesis) { - function buttonsStyles( - speechesButtons, - action = "add", - className = "alekpet_extras_node_speech_icon_playing" - ) { - speechesButtons?.forEach((speechButton) => - speechButton?.classList[action](className) - ); - - const settingTestSpeech = document.querySelector( - ".panel_settings_recognation_speech_voice_test" - ); - if (settingTestSpeech) { - if (action === "add") { - settingTestSpeech.style.opacity = 0.7; - settingTestSpeech.style.color = "var(--error-text)"; - settingTestSpeech.title = "Cancel test speech"; - settingTestSpeech.textContent = "Cancel test"; - } else { - settingTestSpeech.style.opacity = 1; - settingTestSpeech.title = "Run test speech"; - settingTestSpeech.textContent = "Test speech"; - settingTestSpeech.style.color = "limegreen"; - } - } - } - - buttons.push( - $el("span.alekpet_extras_node_speech_icon", { - onclick: function () { - try { - const info = widget.element.querySelector( - ".alekpet_extras_node_info span" - ); - - const speechesButtons = Array.from( - document.querySelectorAll(".alekpet_extras_node_speech_icon") - ); - - // Already playing - if (speechSynthesis.speaking) { - SpeechSynthesis.cancel(); - - info.textContent = "canceled"; - this.title = "Speak text"; - - buttonsStyles(speechesButtons, "remove"); - setTimeout(() => (info.textContent = ""), 2000); - return; - } - - // Start playing text - const text = widgetsText?.element?.value; - if (text.trim() !== "") { - const utterance = speakSynthesisUtterance(text, { - onend: (e) => { - this.title = "Speak text"; - info.textContent = ""; - - buttonsStyles(speechesButtons, "remove"); - }, - }); - - if (utterance) { - this.title = "Cancel speech"; - info.textContent = "saying now"; - - buttonsStyles(speechesButtons); - SpeechSynthesis.speak(utterance); - } - } else { - const utterance = speakSynthesisUtterance( - `${widgetsText?.name || "This"}}, field is empty!`, - { - onend: (e) => { - this.title = "Speak text"; - info.textContent = ""; - - buttonsStyles(speechesButtons, "remove"); - }, - } - ); - - if (utterance) { - this.title = "Cancel speech"; - info.textContent = "empty text!"; - buttonsStyles(speechesButtons); - SpeechSynthesis.speak(utterance); - } - } - } catch (err) { - console.log(err); - } - }, - textContent: "🔊", - title: "Speak text", - }) - ); - } - - widget.element = $el( - "div.alekpet_extras_node_speechrecognition_box", - { style: { display: "none" } }, - [ - $el("div.alekpet_extras_node_speechrecognition_row", [...buttons]), - $el("div.alekpet_extras_node_info", [ - $el("span", { - textContent: "", - style: { fontSize: "0.4em" }, - }), - ]), - ] - ); - - const collapse = node.collapse; - node.collapse = function () { - collapse.apply(this, arguments); - if (this.flags?.collapsed) { - widget.element.hidden = true; - widget.element.style.display = "none"; - } - widget.element.dataset.collapsed = this.flags?.collapsed ? "true" : "false"; - }; - - const onRemovedOrig = node.onRemoved; - node.onRemoved = function () { - node?.widgets?.forEach((w) => { - if (w.type === "speak_and_recognation_type") { - w?.onRemove(); - } - }); - onRemovedOrig?.apply(node, arguments); - }; - - document.body.appendChild(widget.element); - - return widget; -} -/* ~~~ end - Speech & Recognition speech Widget ~~~ */ - /* ~~~ Color Widget ~~~ */ function makeColorWidget(node, inputName, inputData, widget) { const color_hex = $el("input", { @@ -781,12 +106,4 @@ function createPreiviewSize(node, name, options) { } /* ~~~ end - Preview Image Size Widget ~~~ */ -export { - SpeechWidget, - makeColorWidget, - createPreiviewSize, - speechRect, - SpeechSynthesis, - speakSynthesisUtterance, - checkboxLSCheckedByKey, -}; +export { makeColorWidget, createPreiviewSize }; diff --git a/ExtrasNode/lib/extras_node_dialogs.js b/ExtrasNode/lib/extras_speech_and_recognation_dialog.js similarity index 95% rename from ExtrasNode/lib/extras_node_dialogs.js rename to ExtrasNode/lib/extras_speech_and_recognation_dialog.js index 32fd7a1..e092ed2 100644 --- a/ExtrasNode/lib/extras_node_dialogs.js +++ b/ExtrasNode/lib/extras_speech_and_recognation_dialog.js @@ -1,6 +1,12 @@ +/* + * Title: Extras extension - Speech And Recognition Dialog + * Author: AlekPet + * Github: https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ExtrasNode + */ + import { ComfyDialog } from "../../../../../scripts/ui.js"; import { makeElement } from "../../utils.js"; -import { speakSynthesisUtterance } from "./extras_node_widgets.js"; +import { speakSynthesisUtterance } from "./extras_speech_and_recognation_widget.js"; const idExt = "alekpet.ExtrasNode"; @@ -34,7 +40,7 @@ export class RecognationSpeechDialog extends ComfyDialog { class: ["panel_settings_recognation_speech"], style: { display: "grid", - gridTemplateColumns: "1fr 1fr 1fr", + gridTemplateColumns: "0.2fr 1fr 0.2fr", alignItems: "center", justifyContent: "center", gap: "5px", @@ -198,8 +204,9 @@ export class RecognationSpeechDialog extends ComfyDialog { const box = makeElement("div", { class: ["box_recognation_speech"], style: { - width: "25vw", + width: "80vw", color: "var(--input-text)", + padding: "20px", }, children: [await this.createPanelSettings()], }); diff --git a/ExtrasNode/lib/extras_speech_and_recognation_widget.js b/ExtrasNode/lib/extras_speech_and_recognation_widget.js new file mode 100644 index 0000000..da37e60 --- /dev/null +++ b/ExtrasNode/lib/extras_speech_and_recognation_widget.js @@ -0,0 +1,691 @@ +/* + * Title: Extras Speech And Recognation Widget + * Author: AlekPet + * Github: https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ExtrasNode + */ + +import { api } from "../../../../scripts/api.js"; +import { app } from "../../../../scripts/app.js"; +import { $el } from "../../../../scripts/ui.js"; +import { + createWindowModal, + animateClick, + comfyuiDesktopAlert, +} from "../../utils.js"; +import { RecognationSpeechDialog } from "./extras_speech_and_recognation_dialog.js"; + +const idExt = "alekpet.ExtrasNode"; + +/* ~~~ Speech & Recognition speech Widget ~~~ */ +const spRecognition = + window.SpeechRecognition || window.webkitSpeechRecognition; +const SpeechSynthesis = window.speechSynthesis; + +const regExpFileName = /[^\p{L}\d]+/gu; +const maxLenAudioFileName = 50; + +let speechRect; +let mediaRecorder = null; +let audioChunks = []; + +if (spRecognition) { + speechRect = new spRecognition(); + speechRect.elements = null; + speechRect.isRecognition = false; + speechRect.isRecognitionAbort = false; + speechRect.lastText = ""; + + speechRect.addEventListener("result", (event) => { + const results = event?.results[0][0]?.transcript; + if (results && speechRect?.elements?.length) { + const clear_text = speechRect.elements[0].querySelector( + ".alekpet_extras_node_recognition_clear" + ); + + if (clear_text.checked) { + speechRect.elements[1].value = results; + } else { + speechRect.elements[1].value += " " + results; + } + } + }); + + const resetSpeechRecognition = () => { + if (speechRect?.elements?.length) { + const info = speechRect.elements[0].querySelector( + ".alekpet_extras_node_info span" + ); + const icon_rec = speechRect.elements[0].querySelector( + ".alekpet_extras_node_recognition_icon" + ); + + speechRect.elements[0].querySelector( + ".alekpet_extras_node_recognition_aborted" + ).style.display = "none"; + + icon_rec.classList.remove("alekpet_extras_node_recognition_icon_active"); + setTimeout(() => (info.textContent = ""), 1000); + } + + setStylesAllElements(".alekpet_extras_node_recognition_icon", null, { + display: "inline-block", + }); + + if (mediaRecorder) { + speechRect.lastText = speechRect?.elements[1].value; + mediaRecorder.stop(); + } else { + speechRect.lastText = ""; + speechRect.isRecognitionAbort = false; + } + + mediaRecorder = null; + audioChunks = []; + speechRect.isRecognition = false; + + speechRect.elements = null; + }; + + speechRect.addEventListener("audiostart", (e) => { + speechRect.isRecognition = true; + }); + + speechRect.addEventListener("end", () => { + resetSpeechRecognition(); + speechRect.stop(); + }); + + speechRect.addEventListener("speechend", () => { + resetSpeechRecognition(); + speechRect.stop(); + }); + + speechRect.addEventListener("error", (event) => { + if (speechRect?.elements?.length) { + const info = speechRect.elements[0].querySelector( + ".alekpet_extras_node_info span" + ); + info.textContent = event.error; + } + resetSpeechRecognition(); + console.log(">> Error recognition: " + event.error); + }); +} else { + console.warn( + "Your browser does not support: SpeechRecognition.\nIf Firefox: go to about:config > media.webspeech.recognition.enable and media.webspeech.recognition.force_enable > set true" + ); +} + +if (!SpeechSynthesis) { + console.warn("Your browser does not support: speechSynthesis"); +} + +// Get position speech SpeechRecognition widget +function getPostition(ctx, w_width, y, n_height, wInput) { + const MARGIN = 10; + + const rect = ctx.canvas.getBoundingClientRect(); + const transform = new DOMMatrix() + .scaleSelf(rect.width / ctx.canvas.width, rect.height / ctx.canvas.height) + .multiplySelf(ctx.getTransform()) + .translateSelf(MARGIN, MARGIN); + const scale = new DOMMatrix().scaleSelf(transform.a, transform.d); + + return { + transformOrigin: "0 0", + transform: scale, + transform: transform, + left: `${transform.a * w_width - 70 * scale.a + rect.left}px`, + top: `${(wInput.last_y - 15) * scale.d + scale.f + rect.top}px`, + maxWidth: `${w_width - MARGIN * 2}px`, + maxHeight: `${n_height - MARGIN * 2}px`, + zIndex: wInput?.inputEl?.style?.zIndex + ? +wInput?.inputEl.style.zIndex + 1 + : 20, + }; +} + +function getVoiceAndSettings() { + const voices = speechSynthesis.getVoices(); + + const { voice, volume, pitch, rate } = + RecognationSpeechDialog.getSettingsRecSpeechLS(); + const voiceSelected = voices.filter((v) => v.name === voice); + + return { + voice: voiceSelected.length ? voiceSelected[0] : null, + volume, + pitch, + rate, + }; +} + +// Function return utterance +function speakSynthesisUtterance(text, options = {}) { + const utterance = new SpeechSynthesisUtterance(text); + + Object.assign(utterance, { ...options, ...getVoiceAndSettings() }); + return utterance; +} + +// Check premissions +async function checkPremissions( + device = { name: "microphone" }, + update = null +) { + return navigator.permissions + .query(device) + .then((result) => { + const state = result.state; + if (state == "granted") { + return { device, state, status: true }; + } else if (state == "prompt") { + return { device, state, status: false }; + } else if (state == "denied") { + return { device, state, status: false }; + } + result.onchange = update; + }) + .catch((e) => ({ device, state: "error", status: false })); +} + +// Check checkbox +function checkboxLSCheckedByKey(lsKey, selector, defaultVal = false) { + const currValue = JSON.parse(localStorage.getItem(lsKey, defaultVal)); + + Array.from(document.querySelectorAll(selector)).forEach( + (saveAs) => (saveAs.checked = currValue) + ); +} + +// Set styles +const setStylesAllElements = (selector, exclude = null, styles = {}) => { + let elements = Array.from(document.querySelectorAll(selector)); + if (exclude) elements = elements.filter((r) => r !== exclude); + elements = elements.map((r) => Object.assign(r.style, styles)); +}; + +function SpeechWidget(node, inputName, inputData, widgetsText) { + const widget = { + type: "speak_and_recognation_type", + name: inputName, + value: inputData, + size: [22, 1], + options: { hideOnZoom: true }, + text_element: widgetsText?.inputEl, + draw(ctx, node, widget_width, y, widget_height) { + const hidden = widgetsText?.element?.hidden || widgetsText.hidden; + + widget.element.style.display = + hidden || app.canvas.ds.scale < 0.6 ? "none" : "flex"; + if (hidden) { + widget.options.onHide?.(widget); + } + + if (hidden) { + return; + } + + Object.assign( + widget.element.style, + getPostition(ctx, widget_width, y, node.size[1], widgetsText) + ); + }, + computeSize(...args) { + return [22, 1]; + }, + async callback(v) { + if (widgetsText?.element?.hasAttribute("readonly")) return; + + widget.value = v ?? inputData ?? [false, true]; + + const checkboxSave = widget.element.querySelector( + ".alekpet_extras_node_recognition_save" + ); + const checkboxClear = widget.element.querySelector( + ".alekpet_extras_node_recognition_clear" + ); + + if (checkboxClear) checkboxClear.checked = widget.value[1] ?? false; + if (checkboxSave) checkboxSave.checked = widget.value[0] ?? false; + }, + onRemove() { + widget.element?.remove(); + }, + }; + + const buttons = []; + + if (speechRect && !widgetsText?.element?.hasAttribute("readonly")) { + buttons.push( + $el("div.alekpet_extras_node_recognition_icon_box", [ + $el("span.alekpet_extras_node_recognition_aborted", { + title: "Speech recognition abort", + textContent: "✖", + style: { display: "none" }, + onclick: async function (e) { + const info = widget.element.querySelector( + ".alekpet_extras_node_info span" + ); + + if (speechRect.isRecognition) { + e.currentTarget.style.display = "none"; + speechRect.isRecognitionAbort = true; + speechRect.abort(); + info.textContent = "aborted"; + this.classList.remove( + "alekpet_extras_node_recognition_icon_active" + ); + setTimeout(() => (info.textContent = ""), 2000); + } + }, + }), + $el("span.alekpet_extras_node_recognition_icon", { + title: "Speech recognition", + onclick: async function (e) { + const info = widget.element.querySelector( + ".alekpet_extras_node_info span" + ); + const abort = widget.element.querySelector( + ".alekpet_extras_node_recognition_aborted" + ); + const checkboxSave = widget.element.querySelector( + ".alekpet_extras_node_recognition_save" + ); + + // Hide other recognitions buttons + setStylesAllElements( + ".alekpet_extras_node_recognition_icon", + e.currentTarget, + { + display: "none", + } + ); + + // Recognition + if (speechRect.elements === null) { + // Record audio + if (checkboxSave.checked) { + try { + const stream = await navigator.mediaDevices.getUserMedia({ + audio: true, + }); + + mediaRecorder = new MediaRecorder(stream); + + mediaRecorder.ondataavailable = (event) => { + audioChunks.push(event.data); + }; + + mediaRecorder.onstop = async () => { + if (speechRect.isRecognitionAbort) { + speechRect.lastText = ""; + speechRect.isRecognitionAbort = false; + return; + } + + const saveAsWindow = JSON.parse( + localStorage.getItem( + `${idExt}.SpeechAndRecognationSpeechSaveAs`, + false + ) + ); + + // Filename + let nameFile = "recording.webm"; + if (speechRect?.lastText?.length) { + nameFile = `${speechRect.lastText + .slice(0, maxLenAudioFileName) + .replaceAll(regExpFileName, "_")}.webm`; + } + + // Get audio + const audioBlob = new Blob(audioChunks, { + type: "audio/webm", + }); + + if (!saveAsWindow) { + const body = new FormData(); + body.append("image", audioBlob, nameFile); + body.append("overwrite", "true"); + const resp = await api.fetchApi("/upload/image", { + method: "POST", + body, + }); + + if (resp.status !== 200) { + console.error( + "[ExtrasNode] Recording audio not saved!" + ); + return; + } + + console.log( + `[ExtrasNode] Recording audio "${nameFile}" saved successfully!` + ); + } else { + const audioUrl = URL.createObjectURL(audioBlob); + const linkDown = document.createElement("a"); + linkDown.href = audioUrl; + linkDown.download = nameFile; + linkDown.click(); + } + + speechRect.lastText = ""; + }; + } catch (err) { + comfyuiDesktopAlert( + `Device "Microphone" - ${err.message}!\n\nCheck device or allow access!` + ); + } + } + // end - Record audio + + speechRect.elements = [widget.element, widgetsText.inputEl]; + info.textContent = "recognition"; + this.classList.add("alekpet_extras_node_recognition_icon_active"); + speechRect.start(); + mediaRecorder && mediaRecorder.start(); + abort.style.display = "inline-block"; + } else { + speechRect.stop(); + abort.style.display = "none"; + info.textContent = "stoped"; + this.classList.remove( + "alekpet_extras_node_recognition_icon_active" + ); + setTimeout(() => (info.textContent = ""), 2000); + } + }, + }), + // Settings elements + $el("div.alekpet_extras_node_speech_recognition_settings", [ + $el( + "div.alekpet_extras_node_speech_recognition_settings_button.pi.pi-cog", + { + title: "Settings", + onclick: (e) => { + checkboxLSCheckedByKey( + `${idExt}.SpeechAndRecognationSpeechSaveAs`, + ".alekpet_extras_node_recognition_saveAs" + ); + animateClick(e.currentTarget.nextElementSibling); + }, + } + ), + createWindowModal({ + textTitle: null, + stylesClose: { + top: "-4px", + right: "4px", + width: "auto", + height: "auto", + padding: "2px", + fontSize: "0.4rem", + lineHeight: 1, + }, + stylesBox: { + background: "transparent", + border: 0, + padding: 0, + boxShadow: "none", + }, + stylesBody: { + display: "flex", + flexDirection: "column", + alignItems: "stretch", + gap: "3px", + textWrap: "wrap", + background: "rgb(131, 131, 131)", + color: "white", + padding: "2px", + marginTop: "2px", + borderRadius: "6px", + fontSize: "0.4rem", + minWidth: "85px", + }, + stylesWrapper: { + minWidth: "100px", + transform: "translate(0%, 0%)", + }, + textBody: [ + $el( + "label.alekpet_extras_node_speech_recognition_settings_props_label", + [ + $el( + "span.alekpet_extras_node_speech_recognition_settings_props_name", + { + textContent: "Recoding audio", + title: "Save in audio file after recognition", + } + ), + $el( + "input.alekpet_extras_node_speech_recognition_settings_props_input.alekpet_extras_node_speech_recognition_checkbox.alekpet_extras_node_recognition_save", + { + type: "checkbox", + checked: widget.value[0] ?? false, + onchange: (e) => + widget?.callback([!!e.target.checked, widget.value[1]]), + } + ), + ] + ), + $el( + "label.alekpet_extras_node_speech_recognition_settings_props_label", + [ + $el( + "span.alekpet_extras_node_speech_recognition_settings_props_name", + { + textContent: "Clear text", + title: "Clear text after recognition", + } + ), + $el( + "input.alekpet_extras_node_speech_recognition_settings_props_input.alekpet_extras_node_speech_recognition_checkbox.alekpet_extras_node_recognition_clear", + { + type: "checkbox", + checked: widget.value[1] ?? true, + onchange: (e) => + widget?.callback([widget.value[0], !!e.target.checked]), + } + ), + ] + ), + $el("hr", { style: { padding: 0, margin: 0 } }), + $el( + "label.alekpet_extras_node_speech_recognition_settings_props_label", + [ + $el( + "span.alekpet_extras_node_speech_recognition_settings_props_name", + { + textContent: "'Save as' window?", + title: "Show modal window when saving recorded audio.", + } + ), + $el( + "input.alekpet_extras_node_speech_recognition_settings_props_input.alekpet_extras_node_speech_recognition_checkbox.alekpet_extras_node_recognition_saveAs", + { + type: "checkbox", + checked: JSON.parse( + localStorage.getItem( + `${idExt}.SpeechAndRecognationSpeechSaveAs`, + false + ) + ), + onchange: (e) => { + const check = !!e.target.checked; + const settCheck = document.body.querySelector( + "[id$='.SpeechAndRecognationSpeech'] div input" + ); + settCheck && (settCheck.checked = check); + + localStorage.setItem( + `${idExt}.SpeechAndRecognationSpeechSaveAs`, + check + ); + + checkboxLSCheckedByKey( + `${idExt}.SpeechAndRecognationSpeechSaveAs`, + ".alekpet_extras_node_recognition_saveAs" + ); + }, + } + ), + ] + ), + ], + }), + ]), + // end -- Settings elements + ]) + ); + } + + if (SpeechSynthesis && speechSynthesis) { + function buttonsStyles( + speechesButtons, + action = "add", + className = "alekpet_extras_node_speech_icon_playing" + ) { + speechesButtons?.forEach((speechButton) => + speechButton?.classList[action](className) + ); + + const settingTestSpeech = document.querySelector( + ".panel_settings_recognation_speech_voice_test" + ); + if (settingTestSpeech) { + if (action === "add") { + settingTestSpeech.style.opacity = 0.7; + settingTestSpeech.style.color = "var(--error-text)"; + settingTestSpeech.title = "Cancel test speech"; + settingTestSpeech.textContent = "Cancel test"; + } else { + settingTestSpeech.style.opacity = 1; + settingTestSpeech.title = "Run test speech"; + settingTestSpeech.textContent = "Test speech"; + settingTestSpeech.style.color = "limegreen"; + } + } + } + + buttons.push( + $el("span.alekpet_extras_node_speech_icon", { + onclick: function () { + try { + const info = widget.element.querySelector( + ".alekpet_extras_node_info span" + ); + + const speechesButtons = Array.from( + document.querySelectorAll(".alekpet_extras_node_speech_icon") + ); + + // Already playing + if (speechSynthesis.speaking) { + SpeechSynthesis.cancel(); + + info.textContent = "canceled"; + this.title = "Speak text"; + + buttonsStyles(speechesButtons, "remove"); + setTimeout(() => (info.textContent = ""), 2000); + return; + } + + // Start playing text + const text = widgetsText?.element?.value; + if (text.trim() !== "") { + const utterance = speakSynthesisUtterance(text, { + onend: (e) => { + this.title = "Speak text"; + info.textContent = ""; + + buttonsStyles(speechesButtons, "remove"); + }, + }); + + if (utterance) { + this.title = "Cancel speech"; + info.textContent = "saying now"; + + buttonsStyles(speechesButtons); + SpeechSynthesis.speak(utterance); + } + } else { + const utterance = speakSynthesisUtterance( + `${widgetsText?.name || "This"}}, field is empty!`, + { + onend: (e) => { + this.title = "Speak text"; + info.textContent = ""; + + buttonsStyles(speechesButtons, "remove"); + }, + } + ); + + if (utterance) { + this.title = "Cancel speech"; + info.textContent = "empty text!"; + buttonsStyles(speechesButtons); + SpeechSynthesis.speak(utterance); + } + } + } catch (err) { + console.log(err); + } + }, + textContent: "🔊", + title: "Speak text", + }) + ); + } + + widget.element = $el( + "div.alekpet_extras_node_speechrecognition_box", + { style: { display: "none" } }, + [ + $el("div.alekpet_extras_node_speechrecognition_row", [...buttons]), + $el("div.alekpet_extras_node_info", [ + $el("span", { + textContent: "", + style: { fontSize: "0.4em" }, + }), + ]), + ] + ); + + const collapse = node.collapse; + node.collapse = function () { + collapse.apply(this, arguments); + if (this.flags?.collapsed) { + widget.element.hidden = true; + widget.element.style.display = "none"; + } + widget.element.dataset.collapsed = this.flags?.collapsed ? "true" : "false"; + }; + + const onRemovedOrig = node.onRemoved; + node.onRemoved = function () { + node?.widgets?.forEach((w) => { + if (w.type === "speak_and_recognation_type") { + w?.onRemove(); + } + }); + onRemovedOrig?.apply(node, arguments); + }; + + document.body.appendChild(widget.element); + + return widget; +} +/* ~~~ end - Speech & Recognition speech Widget ~~~ */ + +export { + SpeechWidget, + speechRect, + SpeechSynthesis, + speakSynthesisUtterance, + checkboxLSCheckedByKey, +}; diff --git a/README.md b/README.md index f071832..2de242d 100644 --- a/README.md +++ b/README.md @@ -8,26 +8,24 @@ If you enjoy my work, consider **[supporting me](https://alekpet.github.io/suppo # List Nodes: -| Name | Description | ComfyUI category | -| :------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------: | :-----------------------: | -| [**PoseNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/PoseNode) | The node set pose ControlNet | AlekPet Node/image | -| [**PainterNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/PainterNode) | The node set sketch, scrumble image ControlNet and other nodes | AlekPet Node/image | -| [**GoogleTranslateTextNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/GoogleTranslateNode) | The node translate promt uses module **googletrans** from other languages into english and return string | AlekPet Node/conditioning | -| [**GoogleTranslateCLIPTextEncodeNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/GoogleTranslateNode) | The node translate promt uses module **googletrans** from other languages into english, and return conditioning | AlekPet Node/text | -| [**DeepTranslatorTextNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/DeepTranslatorNode) | The node translate promt uses module **Deep Translator** from other languages into english and return string | AlekPet Node/text | -| [**DeepTranslatorCLIPTextEncodeNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/DeepTranslatorNode) | The node translate promt uses module **Deep Translator** from other languages into english, and return conditioning | AlekPet Node/conditioning | -| [**DeepLXTranslateTextNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/DeepLXTranslateNode) | The node translate promt uses **DeepLX** server written for Golang(Go) from other languages into english and return string | AlekPet Node/text | -| [**DeepLXTranslateCLIPTextEncodeNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/DeepLXTranslateNode) | The node translate promt uses **DeepLX** server written for Golang(Go) from other languages into english, and return conditioning | AlekPet Node/conditioning | -| [**ArgosTranslateTextNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ArgosTranslateNode) | The node translate promt uses module **Argos Translator** from other languages into english and return string | AlekPet Node/text | -| [**ArgosTranslateCLIPTextEncodeNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ArgosTranslateNode) | The node translate promt uses module **Argos Translator** from other languages into english, and return conditioning | AlekPet Node/conditioning | -| [**ChatGLM4TranslateTextNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ChatGLMNode) | This translator node uses artificial intelligence to translate prompts and return string | AlekPet Node/text | -| [**ChatGLM4TranslateCLIPTextEncodeNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ChatGLMNode) | This translator node uses artificial intelligence to translate prompts and return conditioning | AlekPet Node/conditioning | -| [**ChatGLM4InstructNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ChatGLMNode) | This node uses artificial intelligence to generate prompt | AlekPet Node/Instruct | -| [**ChatGLM4InstructMediaNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ChatGLMNode) | This node uses artificial intelligence to describe what is shown in the media | AlekPet Node/Instruct | -| [**PreviewTextNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ExtrasNode) | The node displays the input text | AlekPet Node/extras | -| [**ColorsCorrectNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ExtrasNode) | The node for correcting image colors | AlekPet Node/extras | -| [**HexToHueNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ExtrasNode) | The node convert HEX color to HUE (degrees and normal [-0.5, 0.5]) | AlekPet Node/extras | -| [**IDENode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/IDENode) | The node that allows you to run code written in **Python** or **Javascript** directly in the node | AlekPet Node/experiments | +| Name | Description | ComfyUI category | +| :------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------: | :-----------------------: | +| [**PoseNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/PoseNode) | The node set pose ControlNet | AlekPet Node/image | +| [**PainterNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/PainterNode) | The node set sketch, scrumble image ControlNet and other nodes | AlekPet Node/image | +| [**GoogleTranslateTextNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/GoogleTranslateNode) | The node translate promt uses module **googletrans** from other languages into english and return string | AlekPet Node/conditioning | +| [**GoogleTranslateCLIPTextEncodeNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/GoogleTranslateNode) | The node translate promt uses module **googletrans** from other languages into english, and return conditioning | AlekPet Node/text | +| [**DeepTranslatorTextNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/DeepTranslatorNode) | The node translate promt uses module **Deep Translator** from other languages into english and return string | AlekPet Node/text | +| [**DeepTranslatorCLIPTextEncodeNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/DeepTranslatorNode) | The node translate promt uses module **Deep Translator** from other languages into english, and return conditioning | AlekPet Node/conditioning | +| [**ArgosTranslateTextNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ArgosTranslateNode) | The node translate promt uses module **Argos Translator** from other languages into english and return string | AlekPet Node/text | +| [**ArgosTranslateCLIPTextEncodeNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ArgosTranslateNode) | The node translate promt uses module **Argos Translator** from other languages into english, and return conditioning | AlekPet Node/conditioning | +| [**ChatGLM4TranslateTextNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ChatGLMNode) | This translator node uses artificial intelligence to translate prompts and return string | AlekPet Node/text | +| [**ChatGLM4TranslateCLIPTextEncodeNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ChatGLMNode) | This translator node uses artificial intelligence to translate prompts and return conditioning | AlekPet Node/conditioning | +| [**ChatGLM4InstructNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ChatGLMNode) | This node uses artificial intelligence to generate prompt | AlekPet Node/Instruct | +| [**ChatGLM4InstructMediaNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ChatGLMNode) | This node uses artificial intelligence to describe what is shown in the media | AlekPet Node/Instruct | +| [**PreviewTextNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ExtrasNode) | The node displays the input text | AlekPet Node/extras | +| [**ColorsCorrectNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ExtrasNode) | The node for correcting image colors | AlekPet Node/extras | +| [**HexToHueNode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ExtrasNode) | The node convert HEX color to HUE (degrees and normal [-0.5, 0.5]) | AlekPet Node/extras | +| [**IDENode**](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/IDENode) | The node that allows you to run code written in **Python** or **Javascript** directly in the node | AlekPet Node/experiments | # Installing diff --git a/__init__.py b/__init__.py index 246cc50..b616864 100644 --- a/__init__.py +++ b/__init__.py @@ -1,6 +1,6 @@ # Title: ComfyUI Install Customs Nodes and javascript files # Author: AlekPet -# Version: 2024.08.08 +# Version: 2025.06.26 import os import importlib.util import subprocess @@ -32,7 +32,8 @@ extension_dirs = [ DEBUG = False NODE_CLASS_MAPPINGS = dict() # dynamic class nodes append in mappings -NODE_DISPLAY_NAME_MAPPINGS = dict() # dynamic display names nodes append mappings names +# dynamic display names nodes append mappings names +NODE_DISPLAY_NAME_MAPPINGS = dict() humanReadableTextReg = re.compile("(?<=[a-z0-9])([A-Z])|(?<=[A-Z0-9])([A-Z][a-z]+)") module_name_cut_version = re.compile("[>=<]") @@ -47,9 +48,7 @@ def get_version_extension(): if os.path.isfile(toml_file): try: with open(toml_file, "r") as v: - version = list( - filter(lambda l: l.startswith("version"), v.readlines()) - )[0] + version = list(filter(lambda l: l.startswith("version"), v.readlines()))[0] version = version.split("=")[1].replace('"', "").strip() return f" \033[1;34mv{version}\033[0m\033[1;35m" except Exception as e: @@ -69,7 +68,7 @@ def information(datas): try: print(info, end="\r", flush=True) except UnicodeError: - safe_text = info.encode('utf-8', errors='replace').decode('utf-8', errors='replace') + safe_text = info.encode("utf-8", errors="replace").decode("utf-8", errors="replace") print(safe_text, end="\r", flush=True) @@ -80,11 +79,7 @@ def printColorInfo(text, color="\033[92m"): def get_classes(code): tree = ast.parse(code) - return [ - n.name - for n in ast.walk(tree) - if isinstance(n, ast.ClassDef) and "Node" in n.name - ] + return [n.name for n in ast.walk(tree) if isinstance(n, ast.ClassDef) and "Node" in n.name] def addComfyUINodesToMapping(nodeElement): @@ -102,9 +97,7 @@ def addComfyUINodesToMapping(nodeElement): # remove extensions .py module_without_py = f.replace(ext[1], "") # Import module - spec = importlib.util.spec_from_file_location( - module_without_py, os.path.join(node_folder, f) - ) + spec = importlib.util.spec_from_file_location(module_without_py, os.path.join(node_folder, f)) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) classes_names = list( @@ -115,22 +108,11 @@ def addComfyUINodesToMapping(nodeElement): ) for class_module_name in classes_names: # Check module - if ( - class_module_name - and class_module_name not in NODE_CLASS_MAPPINGS.keys() - ): - log( - f" [*] Class node found '{class_module_name}' add to NODE_CLASS_MAPPINGS..." - ) - NODE_CLASS_MAPPINGS.update( - {class_module_name: getattr(module, class_module_name)} - ) + if class_module_name and class_module_name not in NODE_CLASS_MAPPINGS.keys(): + log(f" [*] Class node found '{class_module_name}' add to NODE_CLASS_MAPPINGS...") + NODE_CLASS_MAPPINGS.update({class_module_name: getattr(module, class_module_name)}) NODE_DISPLAY_NAME_MAPPINGS.update( - { - class_module_name: humanReadableTextReg.sub( - " \\1\\2", class_module_name - ) - } + {class_module_name: humanReadableTextReg.sub(" \\1\\2", class_module_name)} ) @@ -168,8 +150,8 @@ def module_install(commands, cwd="."): stderr=subprocess.PIPE, text=True, bufsize=1, - encoding='utf-8', - errors='replace' + encoding="utf-8", + errors="replace", ) out = threading.Thread(target=information, args=(result.stdout,)) err = threading.Thread(target=information, args=(result.stderr,)) @@ -197,17 +179,13 @@ def checkModules(nodeElement): log(" -> File 'requirements.txt' found!") with open(file_requir) as f: required_modules = { - module_name_cut_version.split(line.strip())[0] - for line in f - if not line.startswith("#") + module_name_cut_version.split(line.strip())[0] for line in f if not line.startswith("#") } modules_to_install = required_modules - installed_modules if modules_to_install: - module_install( - [sys.executable, "-m", "pip", "install", *modules_to_install] - ) + module_install([sys.executable, "-m", "pip", "install", *modules_to_install]) nodes_list_dict = {} @@ -318,7 +296,8 @@ try: ) except Exception as e: - nodes_list_dict["ArgosTranslateNode"]["error"] = e + if "ArgosTranslateNode" in nodes_list_dict: + nodes_list_dict["ArgosTranslateNode"]["error"] = e # DeepTranslatorNode @@ -342,31 +321,8 @@ try: ) except Exception as e: - nodes_list_dict["DeepTranslatorNode"]["error"] = e - - -# DeepLXTranslateNode -try: - from .DeepLXTranslateNode.deeplx_translate_node import ( - DeepLXTranslateCLIPTextEncodeNode, - DeepLXTranslateTextNode, - ) - - NODE_CLASS_MAPPINGS.update( - { - "DeepLXTranslateCLIPTextEncodeNode": DeepLXTranslateCLIPTextEncodeNode, - "DeepLXTranslateTextNode": DeepLXTranslateTextNode, - } - ) - NODE_DISPLAY_NAME_MAPPINGS.update( - { - "DeepLXTranslateCLIPTextEncodeNode": "DeepLX Translate CLIP Text Encode Node", - "DeepLXTranslateTextNode": "DeepLX Translate Text Node", - } - ) - -except Exception as e: - nodes_list_dict["DeepLXTranslateNode"]["error"] = e + if "DeepTranslatorNode" in nodes_list_dict: + nodes_list_dict["DeepTranslatorNode"]["error"] = e # GoogleTranslateNode @@ -390,83 +346,157 @@ try: ) except Exception as e: - nodes_list_dict["GoogleTranslateNode"]["error"] = e + if "GoogleTranslateNode" in nodes_list_dict: + nodes_list_dict["GoogleTranslateNode"]["error"] = e + # ChatGLMNode -from .ChatGLMNode.chatglm_node import ( - ChatGLM4TranslateCLIPTextEncodeNode, - ChatGLM4TranslateTextNode, - ChatGLM4InstructNode, - ChatGLM4InstructMediaNode, -) +try: + from .ChatGLMNode.chatglm_node import ( + ChatGLM4TranslateCLIPTextEncodeNode, + ChatGLM4TranslateTextNode, + ChatGLM4InstructNode, + ChatGLM4InstructMediaNode, + ) + + NODE_CLASS_MAPPINGS.update( + { + "ChatGLM4TranslateCLIPTextEncodeNode": ChatGLM4TranslateCLIPTextEncodeNode, + "ChatGLM4TranslateTextNode": ChatGLM4TranslateTextNode, + "ChatGLM4InstructNode": ChatGLM4InstructNode, + "ChatGLM4InstructMediaNode": ChatGLM4InstructMediaNode, + } + ) + + NODE_DISPLAY_NAME_MAPPINGS.update( + { + "ChatGLM4TranslateCLIPTextEncodeNode": "ChatGLM-4 Translate CLIP Text Encode Node", + "ChatGLM4TranslateTextNode": "ChatGLM-4 Translate Text Node", + "ChatGLM4InstructNode": "ChatGLM-4 Instruct Node", + "ChatGLM4InstructMediaNode": "ChatGLM-4 Instruct Media Node", + } + ) + +except Exception as e: + if "ChatGLMNode" in nodes_list_dict: + nodes_list_dict["ChatGLMNode"]["error"] = e + # ExtrasNode -from .ExtrasNode.extras_node import PreviewTextNode, HexToHueNode, ColorsCorrectNode +try: + from .ExtrasNode.extras_node import PreviewTextNode, HexToHueNode, ColorsCorrectNode + + NODE_CLASS_MAPPINGS.update( + { + "PreviewTextNode": PreviewTextNode, + "HexToHueNode": HexToHueNode, + "ColorsCorrectNode": ColorsCorrectNode, + } + ) + NODE_DISPLAY_NAME_MAPPINGS.update( + { + "PreviewTextNode": "Preview Text Node", + "HexToHueNode": "HEX to HUE Node", + "ColorsCorrectNode": "Colors Correct Node", + } + ) +except Exception as e: + if "ExtrasNode" in nodes_list_dict: + nodes_list_dict["ExtrasNode"]["error"] = e + # PainterNode -from .PainterNode.painter_node import PainterNode +try: + from .PainterNode.painter_node import PainterNode + + NODE_CLASS_MAPPINGS.update( + { + "PainterNode": PainterNode, + } + ) + NODE_DISPLAY_NAME_MAPPINGS.update( + { + "PainterNode": "Painter Node", + } + ) + +except Exception as e: + if "PainterNode" in nodes_list_dict: + nodes_list_dict["PainterNode"]["error"] = e + # PoseNode -from .PoseNode.pose_node import PoseNode +try: + from .PoseNode.pose_node import PoseNode + + NODE_CLASS_MAPPINGS.update( + { + "PoseNode": PoseNode, + } + ) + + NODE_DISPLAY_NAME_MAPPINGS.update( + { + "PoseNode": "Pose Node", + } + ) + +except Exception as e: + if "PoseNode" in nodes_list_dict: + nodes_list_dict["PoseNode"]["error"] = e + # IDENode -from .IDENode.ide_node import IDENode +try: + from .IDENode.ide_node import IDENode + NODE_CLASS_MAPPINGS.update( + { + "IDENode": IDENode, + } + ) -NODE_CLASS_MAPPINGS.update( - { - "ChatGLM4TranslateCLIPTextEncodeNode": ChatGLM4TranslateCLIPTextEncodeNode, - "ChatGLM4TranslateTextNode": ChatGLM4TranslateTextNode, - "ChatGLM4InstructNode": ChatGLM4InstructNode, - "ChatGLM4InstructMediaNode": ChatGLM4InstructMediaNode, - "PreviewTextNode": PreviewTextNode, - "HexToHueNode": HexToHueNode, - "ColorsCorrectNode": ColorsCorrectNode, - "PainterNode": PainterNode, - "PoseNode": PoseNode, - "IDENode": IDENode, - } -) + NODE_DISPLAY_NAME_MAPPINGS.update( + { + "IDENode": "IDE Node", + } + ) - -NODE_DISPLAY_NAME_MAPPINGS.update( - { - "ChatGLM4TranslateCLIPTextEncodeNode": "ChatGLM-4 Translate CLIP Text Encode Node", - "ChatGLM4TranslateTextNode": "ChatGLM-4 Translate Text Node", - "ChatGLM4InstructNode": "ChatGLM-4 Instruct Node", - "ChatGLM4InstructMediaNode": "ChatGLM-4 Instruct Media Node", - "PreviewTextNode": "Preview Text Node", - "HexToHueNode": "HEX to HUE Node", - "ColorsCorrectNode": "Colors Correct Node", - "PainterNode": "Painter Node", - "PoseNode": "Pose Node", - "IDENode": "IDE Node", - } -) +except Exception as e: + if "IDENode" in nodes_list_dict: + nodes_list_dict["IDENode"]["error"] = e # Information -printColorInfo( - f"\n### [START] ComfyUI AlekPet Nodes{get_version_extension()} ###", "\033[1;35m" -) +printColorInfo(f"\n### [START] ComfyUI AlekPet Nodes{get_version_extension()} ###", "\033[1;35m") failed_nodes_text = "" len_nodes = len(nodes_list_dict) for key, node in enumerate(nodes_list_dict): currentNode = nodes_list_dict[node] - if currentNode.get("error", None) is None: + + currentNodeVals = currentNode.get("error") + + if currentNodeVals is None: status = "\033[92m[Loading]" else: - status = "\033[1;31;40m[Failed]" + color_error = "\033[1;31;40m" + status = color_error + " [Failed]" error_message = currentNode.get("error", "Error") - failed_nodes_text += f"\033[93m{node} -> \033[1;31;40m{error_message}\033[0m\n" + + if(len(currentNodeVals.args) == 2 and currentNodeVals.args[1] == "warning"): + color_error = "\033[33m" + status = color_error + "[Warning]" + error_message = currentNodeVals.args[0] + + failed_nodes_text += f"\033[93m{node} -> {color_error}{error_message}\033[0m\n" message = currentNode.get("message", "No message available") printColorInfo(f"{message}{status}\033[0m") if key == len_nodes - 1 and failed_nodes_text: printColorInfo( - f"\n\033[1;31;40m* Nodes have been temporarily disabled due to the error *\033[0m\n{failed_nodes_text}" + f"\n\033[1;31;40m* Nodes have been temporarily disabled due to the error or specially *\033[0m\n{failed_nodes_text}" ) printColorInfo(f"### [END] ComfyUI AlekPet Nodes ###", "\033[1;35m") diff --git a/pyproject.toml b/pyproject.toml index e89e862..9e3ba5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui_custom_nodes_alekpet" -description = "Nodes: PoseNode, PainterNode, TranslateTextNode, TranslateCLIPTextEncodeNode, DeepTranslatorTextNode, DeepTranslatorCLIPTextEncodeNode, ArgosTranslateTextNode, ArgosTranslateCLIPTextEncodeNode, ChatGLM4TranslateCLIPTextEncodeNode, ChatGLM4TranslateTextNode, ChatGLM4InstructNode, ChatGLM4InstructMediaNode, DeepLXTranslateTextNode, DeepLXTranslateCLIPTextEncodeNode, PreviewTextNode, HexToHueNode, ColorsCorrectNode, IDENode." -version = "1.0.71" +description = "Nodes: PoseNode, PainterNode, TranslateTextNode, TranslateCLIPTextEncodeNode, DeepTranslatorTextNode, DeepTranslatorCLIPTextEncodeNode, ArgosTranslateTextNode, ArgosTranslateCLIPTextEncodeNode, ChatGLM4TranslateCLIPTextEncodeNode, ChatGLM4TranslateTextNode, ChatGLM4InstructNode, ChatGLM4InstructMediaNode, PreviewTextNode, HexToHueNode, ColorsCorrectNode, IDENode." +version = "1.0.74" license = { file = "LICENSE" } [project.urls]