Files

364 lines
13 KiB
Python

import os
import sys
import tempfile
import torch
import numpy as np
from PIL import Image
import cv2
# Make sure we can import from the same directory
current_dir = os.path.dirname(os.path.abspath(__file__))
if current_dir not in sys.path:
sys.path.append(current_dir)
# Try absolute import with better error handling
try:
from youtube_uploader import YouTubeUploader, GOOGLE_APIS_AVAILABLE
from config import Config
if not GOOGLE_APIS_AVAILABLE:
raise ImportError("Google API libraries not installed")
DEPENDENCIES_AVAILABLE = True
except ImportError as e:
print(f"Warning: YouTube uploader dependencies not available: {e}")
print("Please install dependencies with: pip install -r requirements.txt")
YouTubeUploader = None
Config = None
DEPENDENCIES_AVAILABLE = False
class YouTubeUploaderNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"video": ("IMAGE",), # ComfyUI video as image sequence
"title": ("STRING", {
"multiline": False,
"default": "My Awesome Short #1"
}),
"description": ("STRING", {
"multiline": True,
"default": "Check out this amazing video! 🔥\n\n#Shorts #YouTube #ComfyUI #AI"
}),
"tags": ("STRING", {
"multiline": False,
"default": "shorts,youtube,ai,comfyui,viral"
}),
"privacy": (["private", "unlisted", "public"], {
"default": "private"
}),
"fps": ("INT", {
"default": 30,
"min": 1,
"max": 60,
"step": 1
}),
"upload_enabled": ("BOOLEAN", {
"default": False
}),
},
"optional": {
"audio": ("AUDIO",), # Optional audio input
"thumbnail": ("IMAGE",), # Optional custom thumbnail
}
}
RETURN_TYPES = ("STRING", "STRING", "BOOLEAN")
RETURN_NAMES = ("video_id", "upload_url", "success")
FUNCTION = "upload_to_youtube"
CATEGORY = "YouTube/Upload"
OUTPUT_NODE = True
def __init__(self):
if not DEPENDENCIES_AVAILABLE:
print("YouTube Uploader dependencies not available. Node will return dummy values.")
self.uploader = None
self.config = None
else:
self.uploader = YouTubeUploader()
self.config = Config()
def upload_to_youtube(self, video, title, description, tags, privacy, fps, upload_enabled, audio=None, thumbnail=None):
"""Upload video to YouTube"""
if not DEPENDENCIES_AVAILABLE:
return ("DEPS_NOT_AVAILABLE", "Dependencies not installed", False)
if not upload_enabled:
print("⚠️ YouTube upload is disabled. Set 'upload_enabled' to True to upload.")
return ("UPLOAD_DISABLED", "Upload disabled", False)
try:
# Convert image sequence to video file
video_path = self._create_video_from_images(video, fps, audio)
# Prepare thumbnail if provided
thumbnail_path = None
if thumbnail is not None:
thumbnail_path = self._save_thumbnail(thumbnail)
# Authenticate if needed
if not self.uploader.service:
print("🔐 Authenticating with YouTube...")
self.uploader.authenticate()
# Upload video
print(f"🚀 Uploading video: {title}")
video_id, upload_url, success = self.uploader.upload_video(
video_path, title, description, tags, privacy
)
if success:
print(f"✅ Upload successful! Video ID: {video_id}")
print(f"🔗 Video URL: {upload_url}")
# Set thumbnail if provided
if thumbnail_path and video_id:
self._set_video_thumbnail(video_id, thumbnail_path)
# Cleanup temporary files
self._cleanup_temp_files(video_path, thumbnail_path)
return (video_id, upload_url, True)
else:
print("❌ Upload failed")
self._cleanup_temp_files(video_path, thumbnail_path)
return ("UPLOAD_FAILED", "Upload failed", False)
except Exception as e:
print(f"❌ Error uploading to YouTube: {str(e)}")
return ("ERROR", f"Error: {str(e)}", False)
def _create_video_from_images(self, images, fps, audio=None):
"""Convert ComfyUI image sequence to video file"""
if len(images.shape) == 3:
# Single image, create 3-second video
images = images.unsqueeze(0).repeat(fps * 3, 1, 1, 1)
# Convert tensor to numpy
if isinstance(images, torch.Tensor):
images_np = images.cpu().numpy()
else:
images_np = images
# Ensure values are in 0-255 range
if images_np.max() <= 1.0:
images_np = (images_np * 255).astype(np.uint8)
else:
images_np = images_np.astype(np.uint8)
# Create temporary video file
temp_dir = tempfile.gettempdir()
video_path = os.path.join(temp_dir, f"comfyui_youtube_upload_{hash(str(images_np.shape))}.mp4")
# Get video dimensions
height, width = images_np.shape[1:3]
# Ensure dimensions are even (required for some codecs)
if width % 2 != 0:
width -= 1
if height % 2 != 0:
height -= 1
# Create video writer
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter(video_path, fourcc, fps, (width, height))
try:
for i in range(len(images_np)):
frame = images_np[i]
# Resize if needed
if frame.shape[1] != width or frame.shape[0] != height:
frame = cv2.resize(frame, (width, height))
# Convert RGB to BGR for OpenCV
if len(frame.shape) == 3 and frame.shape[2] == 3:
frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
out.write(frame)
out.release()
# Add audio if provided
if audio is not None:
video_path = self._add_audio_to_video(video_path, audio)
print(f"📹 Created video: {video_path}")
return video_path
except Exception as e:
out.release()
print(f"❌ Error creating video: {str(e)}")
raise
def _add_audio_to_video(self, video_path, audio):
"""Add audio to video using ffmpeg"""
try:
import subprocess
# Save audio to temporary file
audio_path = video_path.replace('.mp4', '_audio.wav')
# Convert audio tensor to wav file
if isinstance(audio, torch.Tensor):
audio_np = audio.cpu().numpy()
else:
audio_np = audio
# Save audio using scipy or librosa if available
try:
import scipy.io.wavfile as wavfile
# Ensure audio is in correct format
if audio_np.max() <= 1.0:
audio_np = (audio_np * 32767).astype(np.int16)
wavfile.write(audio_path, 44100, audio_np)
except ImportError:
print("⚠️ scipy not available, skipping audio")
return video_path
# Combine video and audio
output_path = video_path.replace('.mp4', '_with_audio.mp4')
cmd = [
'ffmpeg', '-y',
'-i', video_path,
'-i', audio_path,
'-c:v', 'copy',
'-c:a', 'aac',
'-shortest',
output_path
]
subprocess.run(cmd, check=True, capture_output=True)
# Cleanup
os.remove(video_path)
os.remove(audio_path)
return output_path
except Exception as e:
print(f"⚠️ Could not add audio: {str(e)}")
return video_path
def _save_thumbnail(self, thumbnail):
"""Save thumbnail image to temporary file"""
if isinstance(thumbnail, torch.Tensor):
thumbnail_np = thumbnail.cpu().numpy()
else:
thumbnail_np = thumbnail
# Convert to PIL Image
if thumbnail_np.max() <= 1.0:
thumbnail_np = (thumbnail_np * 255).astype(np.uint8)
if len(thumbnail_np.shape) == 4:
thumbnail_np = thumbnail_np[0] # Take first image if batch
img = Image.fromarray(thumbnail_np)
# Save to temporary file
temp_dir = tempfile.gettempdir()
thumbnail_path = os.path.join(temp_dir, f"comfyui_thumbnail_{hash(str(thumbnail_np.shape))}.jpg")
img.save(thumbnail_path, 'JPEG', quality=95)
return thumbnail_path
def _set_video_thumbnail(self, video_id, thumbnail_path):
"""Set custom thumbnail for uploaded video"""
try:
# This requires additional YouTube API permissions
self.uploader.service.thumbnails().set(
videoId=video_id,
media_body=thumbnail_path
).execute()
print("🖼️ Custom thumbnail set successfully")
except Exception as e:
print(f"⚠️ Could not set thumbnail: {str(e)}")
def _cleanup_temp_files(self, *file_paths):
"""Clean up temporary files"""
for file_path in file_paths:
if file_path and os.path.exists(file_path):
try:
os.remove(file_path)
except:
pass
class YouTubeAuthNode:
"""Separate node for YouTube authentication"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"client_id": ("STRING", {
"multiline": False,
"default": ""
}),
"client_secret": ("STRING", {
"multiline": False,
"default": ""
}),
"authenticate_now": ("BOOLEAN", {
"default": False
}),
}
}
RETURN_TYPES = ("BOOLEAN", "STRING")
RETURN_NAMES = ("authenticated", "channel_info")
FUNCTION = "authenticate_youtube"
CATEGORY = "YouTube/Auth"
def authenticate_youtube(self, client_id, client_secret, authenticate_now):
"""Authenticate with YouTube API"""
if not DEPENDENCIES_AVAILABLE:
return (False, "Dependencies not installed")
if not authenticate_now:
return (False, "Authentication not requested")
try:
# Create uploader instance
uploader = YouTubeUploader()
# Create client secrets if provided
if client_id and client_secret:
config = Config()
config.CLIENT_ID = client_id
config.CLIENT_SECRET = client_secret
config.PROJECT_ID = "comfyui-youtube-uploader"
config.create_client_secrets_file()
# Authenticate
uploader.authenticate()
# Get channel info
channel_info = uploader.get_channel_info()
if channel_info:
info_text = f"Channel: {channel_info['title']}\nSubscribers: {channel_info['subscriber_count']}\nVideos: {channel_info['video_count']}"
else:
info_text = "Authentication successful"
print(f"✅ YouTube authentication successful!")
print(f"📺 {info_text}")
return (True, info_text)
except Exception as e:
error_msg = f"Authentication failed: {str(e)}"
print(f"❌ {error_msg}")
return (False, error_msg)
# Node mappings for ComfyUI
NODE_CLASS_MAPPINGS = {
"YouTubeUploaderNode": YouTubeUploaderNode,
"YouTubeAuthNode": YouTubeAuthNode
}
NODE_DISPLAY_NAME_MAPPINGS = {
"YouTubeUploaderNode": "🎬 YouTube Uploader",
"YouTubeAuthNode": "🔐 YouTube Auth"
}