From 5119e071ed7591e48315a2125260e295ee877d28 Mon Sep 17 00:00:00 2001 From: BetaDoggo Date: Mon, 5 Aug 2024 00:35:42 -0400 Subject: [PATCH] Add AllInOnePlayer --- README.md | 8 ++- allinoneworkflow.json | 157 ++++++++++++++++++++++++++++++++++++++++++ nodes.py | 105 +++++++++++++++++++++++++++- requirements.txt | 1 + 4 files changed, 268 insertions(+), 3 deletions(-) create mode 100644 allinoneworkflow.json create mode 100644 requirements.txt diff --git a/README.md b/README.md index c8271e0..f3ab334 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,14 @@ # ComfyUI Video Player 1 step closer to replacing all software with comfy ![preview](https://github.com/BetaDoggo/ComfyUI-VideoPlayer/blob/main/ComfyApple.gif) -# Usage + Install this node as well as [ComfyUI-Custom-Scripts](https://github.com/pythongosssss/ComfyUI-Custom-Scripts) +# Usage (automatic) +1. Load allinoneworkflow.json +2. Enter the full video path +3. Check "Extra options" -> "Auto Queue" +4. Click "Queue Prompt" +# Usage (manual) 1. Convert your file using the included convert-video.py or convert-video-color.py/convert-video-color-cuda.py (requires cuda-requirements.txt) 2. Load the example workflow 3. Set the framerate of the video diff --git a/allinoneworkflow.json b/allinoneworkflow.json new file mode 100644 index 0000000..bbf03e2 --- /dev/null +++ b/allinoneworkflow.json @@ -0,0 +1,157 @@ +{ + "last_node_id": 16, + "last_link_id": 11, + "nodes": [ + { + "id": 7, + "type": "ShowText|pysssss", + "pos": [ + 1214, + 338 + ], + "size": { + "0": 1404.90625, + "1": 1109.595703125 + }, + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "text", + "type": "STRING", + "link": 10, + "widget": { + "name": "text" + } + } + ], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": null, + "shape": 6 + } + ], + "properties": { + "Node name for S&R": "ShowText|pysssss" + }, + "widgets_values": [ + "", + "Failed to load frame. Either the video is over, the video path is wrong or there's another error. \nMake sure that you entered a direct path and that there are no \"s in the path." + ] + }, + { + "id": 15, + "type": "AllInOnePlayer", + "pos": [ + 882, + 341 + ], + "size": [ + 315, + 130 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "name": "frame", + "type": "INT", + "link": 11, + "widget": { + "name": "frame" + } + } + ], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 10 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "AllInOnePlayer" + }, + "widgets_values": [ + 1, + "", + 100, + 30 + ] + }, + { + "id": 16, + "type": "PrimitiveNode", + "pos": [ + 883, + 515 + ], + "size": [ + 267.27526413341457, + 82 + ], + "flags": {}, + "order": 0, + "mode": 0, + "outputs": [ + { + "name": "INT", + "type": "INT", + "links": [ + 11 + ], + "slot_index": 0, + "widget": { + "name": "frame" + } + } + ], + "title": "Frame Counter\n", + "properties": { + "Run widget replace on values": false + }, + "widgets_values": [ + 1, + "increment" + ] + } + ], + "links": [ + [ + 10, + 15, + 0, + 7, + 0, + "STRING" + ], + [ + 11, + 16, + 0, + 15, + 0, + "INT" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 0.7513148009015777, + "offset": [ + -552.0969407948444, + -147.57064944942996 + ] + } + }, + "version": 0.4 +} \ No newline at end of file diff --git a/nodes.py b/nodes.py index 34be924..dd7fc2c 100644 --- a/nodes.py +++ b/nodes.py @@ -2,8 +2,10 @@ import os import cv2 import time import torch +import shutil import numpy as np from PIL import Image +from comfy.utils import ProgressBar class LoadFrame: @classmethod @@ -21,6 +23,7 @@ class LoadFrame: CATEGORY = "VideoPlayer" def Loadframe(self, frame, frameRate, path): try: + path = path.replace('"', '') #make "copy as path" faster with open((path + str(frame) + ".txt"), 'r', encoding='utf-8') as file: content = file.read() timestamp = 0.00 @@ -45,8 +48,10 @@ class LoadJPGFrame: RETURN_TYPES = ("IMAGE", "FLOAT",) FUNCTION = "load_jpg_frame" CATEGORY = "VideoPlayer" + def load_jpg_frame(self, frame, frameRate, path): try: + path = path.replace('"', '') #make "copy as path" faster image_path = os.path.join(path, f"{frame:05d}.jpg") with Image.open(image_path) as img: img = img.convert("RGB") @@ -75,8 +80,10 @@ class LoadVideoFrame: RETURN_TYPES = ("IMAGE", "FLOAT",) FUNCTION = "LoadVideoFrame" CATEGORY = "VideoPlayer" + def LoadVideoFrame(self, video_path, frame, frameRate): try: + video_path = video_path.replace('"', '') #make "copy as path" faster cap = cv2.VideoCapture(video_path) cap.set(cv2.CAP_PROP_POS_FRAMES, frame - 1) # Subtract 1 because frame count starts at 0 ret, img = cap.read() @@ -123,17 +130,111 @@ class ImageToEmoji: emoji_array = np.where(pixels > threshold, "⬜", "⬛") ascii_image = '\n'.join([''.join(row) for row in emoji_array]) return(ascii_image,) + +class AllInOnePlayer: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "frame": ("INT", {"default": 1, "min": 1, "max": 100000, "step": 1, "forceInput": True}), + "video_path": ("STRING", {"forceInput": False}), + "width": ("INT", {"default": 100, "min": 10, "max": 200, "step": 1}), + "framerate": ("INT", {"default": 30, "min": 0, "max": 500, "step": 1}), + }, + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "PlayVideo" + CATEGORY = "VideoPlayer" + + def __init__(self): + self.node_dir = os.path.dirname(os.path.abspath(__file__)) + + def ImageToEmoji(self, image, width): + if len(image.shape) == 3: + image = np.mean(image, axis=2).astype(np.uint8) + else: + image = image.astype(np.uint8) + img = Image.fromarray(image) + pixels = np.array(img) + threshold = np.mean(pixels) + emoji_array = np.where(pixels > threshold, "⬜", "⬛") + ascii_image = '\n'.join([''.join(row) for row in emoji_array]) + return ascii_image + + def get_video_prefix(self, video_path): + return os.path.splitext(os.path.basename(video_path))[0] + + def resize_frame(self, frame, target_width): + height, width = frame.shape[:2] + aspect_ratio = height / width + new_height = int(target_width * aspect_ratio) + return cv2.resize(frame, (target_width, new_height), interpolation=cv2.INTER_AREA) + + def ExtractFrames(self, video_path, target_width): + temp_frames_dir = os.path.join(self.node_dir, "temp_frames") + shutil.rmtree(temp_frames_dir, ignore_errors=True) + os.makedirs(temp_frames_dir) + video_prefix = self.get_video_prefix(video_path) + cap = cv2.VideoCapture(video_path) + total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + #Initialize progress bar + progress = ProgressBar(10) + frame_count = 0 + next_progress_update = total_frames // 10 # Calculate frames per 10% + + while True: + ret, frame = cap.read() + if not ret: + break + resized_frame = self.resize_frame(frame, target_width) + emoji_frame = self.ImageToEmoji(resized_frame, target_width) + + frame_filename = os.path.join(temp_frames_dir, f"{video_prefix}_frame_{frame_count:04d}.txt") + with open(frame_filename, 'w', encoding='utf-8') as f: + f.write(emoji_frame) + + frame_count += 1 + #Update progress bar every 10% of frames processed + if frame_count >= next_progress_update: + progress.update(1) + next_progress_update += total_frames // 10 + + cap.release() + + def PlayVideo(self, frame, video_path, width, framerate): + video_path = video_path.replace('"', '') #make "copy as path" faster + progress = ProgressBar(10) + progress.update + temp_frames_dir = os.path.join(self.node_dir, "temp_frames") + video_prefix = self.get_video_prefix(video_path) + + if not os.path.exists(temp_frames_dir) or not any(f.startswith(video_prefix) for f in os.listdir(temp_frames_dir)): + self.ExtractFrames(video_path, width) + + frame_path = os.path.join(temp_frames_dir, f"{video_prefix}_frame_{frame:04d}.txt") + if os.path.exists(frame_path): + with open(frame_path, 'r', encoding='utf-8') as f: + emoji_frame = f.read() + + if framerate != 0: + time.sleep(1/framerate) + return (emoji_frame,) + else: + return ("Failed to load frame. Either the video is over, the video path is wrong, or there's another error.",) NODE_CLASS_MAPPINGS = { "LoadFrame": LoadFrame, "LoadJPGFrame": LoadJPGFrame, "LoadVideoFrame": LoadVideoFrame, - "ImageToEmoji": ImageToEmoji + "ImageToEmoji": ImageToEmoji, + "AllInOnePlayer": AllInOnePlayer, } NODE_DISPLAY_NAME_MAPPINGS = { "LoadFrame": "LoadFrame", "LoadJPGFrame": "LoadJPGFrame", "LoadVideoFrame": "Load Video Frame", - "ImageToEmoji": "Image To Emoji" + "ImageToEmoji": "Image To Emoji", + "AllInOnePlayer": "AllInOnePlayer", } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1db7aea --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +opencv-python \ No newline at end of file