From 8e4a34dce1eaa2050013af214020c6a4225c9a4c Mon Sep 17 00:00:00 2001 From: Richard Allen Date: Tue, 21 Jul 2026 16:50:05 -0400 Subject: [PATCH] update short scripts --- .vscode/launch.json | 5 +- database.py | 2 +- transcribe_video.py | 4 +- twitch_down_uploader.py | 16 ++-- youtube_hashtags.py | 4 +- youtube_short.py | 176 ++++++++++++++++++++++++++++++++++++---- 6 files changed, 179 insertions(+), 28 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 148f8b7..20292ec 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,10 @@ "type": "debugpy", "request": "launch", "program": "${file}", - "args": [] + "args": [ + "--convert", + "/home/richard/downloader/save/clips/VibrantKathishLobsterPunchTrees/VibrantKathishLobsterPunchTrees.mp4" + ] } ] } \ No newline at end of file diff --git a/database.py b/database.py index 8e949f2..9ef6bae 100644 --- a/database.py +++ b/database.py @@ -5,7 +5,7 @@ from datetime import date as datetime_date from pathlib import Path class Database: - def __init__(self, table): + def __init__(self, table: str): self.table = table self.columns = "" diff --git a/transcribe_video.py b/transcribe_video.py index 9417cd5..0b61db2 100644 --- a/transcribe_video.py +++ b/transcribe_video.py @@ -7,7 +7,7 @@ from whisper.utils import get_writer model = whisper.load_model("base") -def extract_audio(video_path, audio_temp_path): +def extract_audio(video_path: str, audio_temp_path: str): # Create a temporary path for a sanitized copy of the video sanitized_video_path = video_path.replace(".mp4", "_clean.mp4") @@ -40,7 +40,7 @@ def extract_audio(video_path, audio_temp_path): os.remove(sanitized_video_path) -def transcribe_to_srt(audio_path, output_directory, output_filename): +def transcribe_to_srt(audio_path: str, output_directory: str, output_filename: str): print("Transcribing audio...") result = model.transcribe(audio_path) diff --git a/twitch_down_uploader.py b/twitch_down_uploader.py index aafe944..a0e5b5a 100644 --- a/twitch_down_uploader.py +++ b/twitch_down_uploader.py @@ -29,9 +29,9 @@ def run_linux_command(command: str): # Handles errors if the Linux command returns a non-zero exit code return {"success": False, "stdout": e.stdout, "stderr": e.stderr} -def transcribe(slug: str): +def transcribe(id: str): """Transcribes the Video File.""" - video_file = f"save/{DB.table}/{slug}/{slug}.mp4" + video_file = f"save/{DB.table}/{id}/{id}.mp4" # Check if the video file exists if not os.path.exists(video_file): @@ -39,18 +39,18 @@ def transcribe(slug: str): return False # no need to continue if srt transcribe file already exists - if os.path.exists(f"save/{DB.table}/{slug}/transcribe_{slug}.srt"): + if os.path.exists(f"save/{DB.table}/{id}/transcribe_{id}.srt"): print(f"video already transcribed:") return True - transcribe_video.extract_audio(video_file, f"save/{DB.table}/{slug}/temp_{slug}_audio.wav") - transcribe_video.transcribe_to_srt(f"save/{DB.table}/{slug}/temp_{slug}_audio.wav", f"save/{DB.table}/{slug}/", f"transcribe_{slug}") + transcribe_video.extract_audio(video_file, f"save/{DB.table}/{id}/temp_{id}_audio.wav") + transcribe_video.transcribe_to_srt(f"save/{DB.table}/{id}/temp_{id}_audio.wav", f"save/{DB.table}/{id}/", f"transcribe_{id}") return True -def top_hashtags(slug: str): +def top_hashtags(id: str): """"Hashtags from transcribed SRT file.""" - file_srt = f"save/{DB.table}/{slug}/transcribe_{slug}.srt" + file_srt = f"save/{DB.table}/{id}/transcribe_{id}.srt" # if srt transcribe file not exists if not os.path.exists(file_srt): @@ -99,7 +99,7 @@ def upload(): unuploaded = DB.get_unuploaded() - twitch_datetime = " #Twitch Every Friday and Sunday @7:30 EST https://twitch.tv/teampgp" + twitch_datetime = " Live on Twitch Every Friday and Sunday @7:30 ET https://twitch.tv/teampgp" file_path = "" title = "" description = "" diff --git a/youtube_hashtags.py b/youtube_hashtags.py index e305ac8..2833f1a 100644 --- a/youtube_hashtags.py +++ b/youtube_hashtags.py @@ -11,7 +11,7 @@ nltk.download('punkt', quiet=True) nltk.download('stopwords', quiet=True) nltk.download('punkt_tab', quiet=True) -def extract_text_from_srt(file_path): +def extract_text_from_srt(file_path: str): with open(file_path, 'r', encoding='utf-8') as file: content = file.read() # Remove SRT timestamps and sequence numbers @@ -19,7 +19,7 @@ def extract_text_from_srt(file_path): clean_text = re.sub(r'\d+', '', clean_text) return clean_text -def get_top_hashtags(srt_file_path, top_n=10): +def get_top_hashtags(srt_file_path: str, top_n: int = 10): raw_text = extract_text_from_srt(srt_file_path) # Lowercase and remove punctuation diff --git a/youtube_short.py b/youtube_short.py index decec51..cc07f77 100644 --- a/youtube_short.py +++ b/youtube_short.py @@ -1,22 +1,157 @@ #!/usr/bin/env python3 import argparse import sys +import subprocess +import tempfile from pathlib import Path -from moviepy import VideoFileClip +from moviepy import VideoFileClip, ColorClip, CompositeVideoClip +from moviepy.video.VideoClip import TextClip -def convert_to_short(input_path): +from pathlib import Path +import numpy as np +from PIL import Image, ImageFilter +def apply_gaussian_blur(frame, radius: int = 30): + """ + Transforms a single NumPy array frame using PIL's true GaussianBlur filter. + """ + # Convert numpy array to PIL Image + image = Image.fromarray(frame) + # Apply high-quality true Gaussian Blur + blurred_image = image.filter(ImageFilter.GaussianBlur(radius=radius)) + # Return back as a numpy array for MoviePy + return np.array(blurred_image) + +def fit_to_9_16_letterbox(input_path: str, top_text: str = "TOP TEXT", bottom_text: str = "BOTTOM TEXT", use_blur: bool = True): input_file = Path(input_path) + output_suffix = "gaussian_9_16" if use_blur else "black_9_16" + output_path = input_file.parent / f"{input_file.stem}_{output_suffix}{input_file.suffix}" - output_path = f"{input_file.stem}_shorts{input_file.suffix}" + print("๐Ÿงผ Sanitizing video metadata streams inside an automated safe context...") + with tempfile.NamedTemporaryFile(suffix=input_file.suffix, delete=False) as temp_file: + temp_path = temp_file.name - # Load the video file - clip = VideoFileClip(input_path) + bg_scaled = None + bg_cropped = None + background_layer = None + + try: + cleanup_cmd = [ + "ffmpeg", "-y", "-i", str(input_file), + "-map_chapters", "-1", "-sn", + "-c", "copy", temp_path + ] + subprocess.run(cleanup_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + # Load video + clip = VideoFileClip(temp_path) + + # Standard vertical 9:16 canvas sizes + canvas_w = 1080 + canvas_h = 1920 + + # Background logic + if use_blur: + print("๐Ÿ“ Scaling, cropping, and blurring background layer...") + bg_scaled = clip.resized(height=canvas_h) + bg_cropped = bg_scaled.cropped(width=canvas_w, x_center=bg_scaled.w / 2) + background_layer = bg_cropped.transform(lambda gf, t: apply_gaussian_blur(gf(t), radius=35)) + else: + print("โšซ Creating solid black background canvas...") + background_layer = ColorClip(size=(canvas_w, canvas_h), color=(0, 0, 0), duration=clip.duration) + + print("๐Ÿ“ Shrinking foreground video width to fit the 1080 wide canvas...") + foreground_clip = clip.resized(width=canvas_w) + + print("โœ๏ธ Creating multi-line top title text clip...") + # FIXED: Added 'method="caption"' and increased vertical size to 300 + title_clip = TextClip( + text=top_text, + font_size=55, # Slightly smaller to accommodate paragraphs comfortably + color="white", + font="DejaVuSans-Bold", + text_align="center", + size=(canvas_w - 100, 300), # Subtracted 100px for safety margins on left/right edges + method="caption", # Forces text to wrap cleanly onto a new line + duration=clip.duration + ) + # Position adjusted to center the taller 300px box in the upper section + positioned_top_text = title_clip.with_position(("center", 180)) + + print("โœ๏ธ Creating multi-line bottom text clip...") + # FIXED: Added 'method="caption"' and increased vertical size to 300 + bottom_clip = TextClip( + text=bottom_text, + font_size=55, + color="white", + font="DejaVuSans-Bold", + text_align="center", + size=(canvas_w - 100, 300), # Left/right margins included + method="caption", # Forces text to wrap cleanly onto a new line + duration=clip.duration + ) + # Position adjusted to center the taller 300px box in the lower section + positioned_bottom_text = bottom_clip.with_position(("center", 1430)) + + # Composite layers + final_clip = CompositeVideoClip( + [ + background_layer, + foreground_clip.with_position("center"), + positioned_top_text, + positioned_bottom_text + ] + ).with_audio(clip.audio) + + print("๐ŸŽฌ Rendering final vertical composition...") + final_clip.write_videofile( + str(output_path), + codec="libx264", + audio_codec="aac", + fps=clip.fps + ) + + # Clean up file locks safely + clip.close() + if bg_scaled: bg_scaled.close() + if bg_cropped: bg_cropped.close() + background_layer.close() + foreground_clip.close() + title_clip.close() + bottom_clip.close() + final_clip.close() + + finally: + temp_file_path = Path(temp_path) + if temp_file_path.exists(): + temp_file_path.unlink() + + print(f"๐ŸŽ‰ Text overlay video saved to: {output_path}") + +def convert_to_short(input_path: str): + input_file = Path(input_path) + + # Generate paths using pathlib + sanitized_input_path = input_file.parent / f"{input_file.stem}_clean{input_file.suffix}" + output_path = input_file.parent / f"{input_file.stem}_shorts{input_file.suffix}" + + print("Sanitizing video metadata for MoviePy parser...") + cleanup_cmd = [ + "ffmpeg", "-y", "-i", str(input_file), + "-map_chapters", "-1", "-sn", + "-c", "copy", str(sanitized_input_path) + ] + subprocess.run(cleanup_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + # Load the sanitized video file + clip = VideoFileClip(str(sanitized_input_path)) # 1. Check video duration if clip.duration >= 60: print(f"โŒ Error: Video is {clip.duration:.2f}s long. It must be under 60 seconds.") clip.close() + if sanitized_input_path.exists(): + sanitized_input_path.unlink() return False print(f"โœ… Video length check passed ({clip.duration:.2f}s). Processing...") @@ -25,28 +160,39 @@ def convert_to_short(input_path): target_height = clip.h target_width = int(target_height * (9 / 16)) - # 3. Crop the center of the video - # x_center centers the crop horizontally - short_clip = clip.crop(width=target_width, height=target_height, x_center=clip.w / 2, y_center=clip.h / 2) + # 3. FIX FOR v2.x: Use native .cropped() method directly on the clip + print("๐ŸŽฌ Cropping video to 9:16 vertical view...") + short_clip = clip.cropped( + width=target_width, + height=target_height, + x_center=clip.w / 2, + y_center=clip.h / 2 + ) - # Optional: Resize to standard YouTube Short resolution (1080x1920) + # 4. FIX FOR v2.x: Use native .resized() method directly on the clip if short_clip.h != 1920: - short_clip = short_clip.resize(newsize=(1080, 1920)) + print("๐Ÿ“ Resizing frame resolution to standard 1080x1920 Shorts format...") + short_clip = short_clip.resized(new_size=(1080, 1920)) - # 4. Export the final video file + # 5. Export the final video file short_clip.write_videofile( - output_path, + str(output_path), codec="libx264", audio_codec="aac", fps=clip.fps ) - # Clean up files + # Clean up files and close streams clip.close() short_clip.close() + + if sanitized_input_path.exists(): + sanitized_input_path.unlink() + print(f"๐ŸŽ‰ Success! Short saved as: {output_path}") return True + def upload_shorts(): """Loops over the downloaded videos entries and uploaded them to youtube.""" print("Uploading Shorts...") @@ -90,7 +236,9 @@ def main(): # Execute conversion step if path is provided if args.convert: - convert_to_short(args.convert) + clip_by = "joelmckinney" + #convert_to_short(args.convert) + fit_to_9_16_letterbox(args.convert, "TeamPGP Live on Twitch...\n Every Friday and Sunday @7:30 ET.\n twitch.tv/teampgp", f"Clipped By: {clip_by}.", True) # Execute database upload step if flag is provided if args.upload: