Add AllInOnePlayer
This commit is contained in:
@@ -1,8 +1,14 @@
|
||||
# ComfyUI Video Player
|
||||
1 step closer to replacing all software with comfy
|
||||

|
||||
# 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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
opencv-python
|
||||
Reference in New Issue
Block a user