Initial YouTube description generation and authentication — work in progress
This commit is contained in:
@ -5,7 +5,8 @@ from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
# debugging flag
|
||||
DEBUG = True
|
||||
DEBUG = os.getenv("DEBUG_MODE", "false").lower() == "true"
|
||||
|
||||
|
||||
# 🔧 Project Root
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
67
modules/description_utils.py
Normal file
67
modules/description_utils.py
Normal file
@ -0,0 +1,67 @@
|
||||
"""
|
||||
description_utils.py
|
||||
|
||||
Utility functions for generating video descriptions dynamically using OpenAI's API.
|
||||
Includes brand-aware humor, format-aware descriptions, and dynamic prompt generation.
|
||||
|
||||
This module currently supports:
|
||||
- Montage descriptions (fun, quirky, "Cool-Hand Gramps" themed)
|
||||
|
||||
Author: Llama Chile Shop
|
||||
Created: 2025-07-22
|
||||
"""
|
||||
|
||||
import os
|
||||
import random
|
||||
import openai
|
||||
|
||||
# 🛠 Global debug flag (imported by design elsewhere)
|
||||
from modules.config import DEBUG
|
||||
|
||||
# Set up OpenAI API key from environment
|
||||
openai.api_key = os.getenv("OPENAI_API_KEY")
|
||||
|
||||
|
||||
def generate_montage_description() -> str:
|
||||
"""
|
||||
Generates a creative, humorous description for a montage highlight video.
|
||||
Leverages the "Cool-Hand Gramps" branding identity and inserts dynamic randomness
|
||||
to keep each description fresh and engaging.
|
||||
|
||||
Returns:
|
||||
str: A YouTube/PeerTube-ready video description.
|
||||
"""
|
||||
# 🎲 Add entropy to reduce prompt caching / same-seed behavior
|
||||
creativity_seed = random.randint(0, 999999)
|
||||
|
||||
# 🧠 Base template for the prompt
|
||||
prompt = f"""
|
||||
You are a branding-savvy copywriter helping a YouTube gaming channel called "Llama Chile Shop"
|
||||
run by a quirky and beloved senior gamer named "Gramps." Gramps is known for his calm demeanor,
|
||||
sharp shooting, and whacky senile playstyle in Solo Zero Build Fortnite matches. His fans refer
|
||||
to him as "Cool-Hand Gramps" because his heart rate doesn’t rise, even in intense firefights.
|
||||
|
||||
Write a YouTube/PeerTube video description for a highlight montage from one of Gramps' livestreams.
|
||||
Make it short, funny, and on-brand. Include emoticons and hashtags. Add a sentence encouraging viewers
|
||||
to subscribe and check out the stream calendar.
|
||||
|
||||
Entropy seed: {creativity_seed}
|
||||
"""
|
||||
|
||||
try:
|
||||
response = openai.ChatCompletion.create(
|
||||
model="gpt-4",
|
||||
messages=[
|
||||
{"role": "system", "content": "You are a creative and humorous copywriter."},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
temperature=0.9,
|
||||
max_tokens=250
|
||||
)
|
||||
return response.choices[0].message.content.strip()
|
||||
|
||||
except Exception as e:
|
||||
fallback = "Join Gramps for another action-packed Fortnite montage! Subscribe and watch live ➡ https://youtube.com/@llamachileshop 🎮🦙 #Fortnite #CoolHandGramps"
|
||||
if DEBUG:
|
||||
print(f"[ERROR] Failed to generate montage description: {e}")
|
||||
return fallback
|
||||
@ -111,5 +111,5 @@ def generate_montage_title(session_name: str) -> str:
|
||||
parts = session_name.split(".")
|
||||
year, month, day = map(int, parts[:3])
|
||||
suffix = f" Video {parts[3]}" if len(parts) > 3 else ""
|
||||
date_str = datetime(year, month, day).strftime("%B %-d, %Y")
|
||||
date_str = datetime(year, month, day).strftime("%B %d, %Y").replace(" 0", " ")
|
||||
return f"#Fortnite #Solo #Zerobuild #Highlights with Gramps from {date_str}{suffix}"
|
||||
|
||||
@ -1,88 +1,103 @@
|
||||
"""
|
||||
yt_poster.py
|
||||
|
||||
Handles video uploads to YouTube using the YouTube Data API.
|
||||
|
||||
This module includes logic for setting titles, descriptions, tags, and
|
||||
privacy status. It integrates with description generation tools and supports
|
||||
automatic metadata based on the video type (e.g., montage).
|
||||
|
||||
Requires authentication via OAuth 2.0 and expects a valid token.pickle file.
|
||||
|
||||
Author: Llama Chile Shop
|
||||
"""
|
||||
|
||||
import os
|
||||
import pickle, logging
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
from google_auth_oauthlib.flow import InstalledAppFlow
|
||||
from google.auth.transport.requests import Request
|
||||
from pathlib import Path
|
||||
from googleapiclient.discovery import build
|
||||
from googleapiclient.errors import HttpError
|
||||
from googleapiclient.http import MediaFileUpload
|
||||
from modules.title_utils import get_output_filename, generate_montage_title
|
||||
from modules.title_utils import generate_montage_title, generate_output_filename
|
||||
from modules.description_utils import generate_montage_description
|
||||
from modules.config import DEBUG
|
||||
|
||||
# Define OAuth scopes and token paths
|
||||
SCOPES = ["https://www.googleapis.com/auth/youtube.upload"]
|
||||
TOKEN_PATH = Path("token.pickle")
|
||||
CLIENT_SECRETS_FILE = Path("client_secrets.json")
|
||||
|
||||
def authenticate_youtube():
|
||||
"""Handles YouTube OAuth flow and returns a service client."""
|
||||
creds = None
|
||||
def upload_video(file_path: Path, is_vertical: bool, stream_date: str, description: str = None, private: bool = DEBUG) -> str:
|
||||
"""
|
||||
Uploads a video file to YouTube.
|
||||
|
||||
if TOKEN_PATH.exists():
|
||||
with open(TOKEN_PATH, "rb") as token_file:
|
||||
creds = pickle.load(token_file)
|
||||
Args:
|
||||
file_path (str): Full path to the rendered video file.
|
||||
is_vertical (bool): True if video is vertical format (9:16), else widescreen (16:9).
|
||||
stream_date (str): Date of the stream in YYYY.MM.DD or YYYY.MM.DD.N format.
|
||||
|
||||
if not creds or not creds.valid:
|
||||
if creds and creds.expired and creds.refresh_token:
|
||||
creds.refresh(Request())
|
||||
else:
|
||||
if not CLIENT_SECRETS_FILE.exists():
|
||||
raise FileNotFoundError("client_secrets.json not found.")
|
||||
flow = InstalledAppFlow.from_client_secrets_file(
|
||||
str(CLIENT_SECRETS_FILE), SCOPES
|
||||
)
|
||||
creds = flow.run_local_server(port=0)
|
||||
with open(TOKEN_PATH, "wb") as token_file:
|
||||
pickle.dump(creds, token_file)
|
||||
Returns:
|
||||
str: URL of the uploaded YouTube video.
|
||||
"""
|
||||
try:
|
||||
# Build title I have this:"and description
|
||||
file_path = str(file_path)
|
||||
session_name = Path(file_path).parents[1].name
|
||||
title = generate_montage_title(session_name)
|
||||
|
||||
if not description:
|
||||
description = str(generate_montage_description())
|
||||
|
||||
return build("youtube", "v3", credentials=creds)
|
||||
# Construct tags and privacy status
|
||||
tags = ["Fortnite", "Zero Build", "Solo", "Gramps", "CoolHandGramps"]
|
||||
privacy_status = "private" if private else "public"
|
||||
|
||||
def generate_description(clip_path: Path, stream_date: datetime, is_montage: bool = False) -> str:
|
||||
"""Creates a dynamic and fun YouTube description."""
|
||||
kill_count_guess = sum(word.isdigit() for word in clip_path.stem.split())
|
||||
date_str = stream_date.strftime("%B %d, %Y")
|
||||
# Authenticate
|
||||
from authorize_youtube import get_authenticated_service
|
||||
youtube = get_authenticated_service()
|
||||
|
||||
intro = "Gramps is back in Fortnite with another spicy highlight! 🦥"
|
||||
if is_montage:
|
||||
body = (
|
||||
f"This reel features an outrageous compilation of top plays from our {date_str} stream.\n"
|
||||
f"{kill_count_guess} eliminations of stupendous magnitude that must be seen to be believed!"
|
||||
)
|
||||
else:
|
||||
body = (
|
||||
f"Recorded live on {date_str}, this clip captures one of many wild moments "
|
||||
"from the battlefield. Grab your popcorn. 🎮"
|
||||
)
|
||||
|
||||
hashtags = "#Fortnite #Gaming #SeniorGamer #LlamaChileShop #EpicMoments"
|
||||
|
||||
return f"{intro}\n\n{body}\n\nSubscribe for more: https://youtube.com/@llamachileshop\n{hashtags}"
|
||||
|
||||
def upload_to_youtube(video_path: Path, title: str, description: str, is_short: bool = False) -> str:
|
||||
"""Uploads the video to YouTube and returns the video URL."""
|
||||
youtube = authenticate_youtube()
|
||||
|
||||
request_body = {
|
||||
"snippet": {
|
||||
"title": title,
|
||||
"description": description,
|
||||
"tags": ["Fortnite", "Gaming", "Senior Gamer", "LlamaChileShop"],
|
||||
"categoryId": "20", # Gaming
|
||||
},
|
||||
"status": {
|
||||
"privacyStatus": "private",
|
||||
"selfDeclaredMadeForKids": False,
|
||||
body = {
|
||||
"snippet": {
|
||||
"title": title,
|
||||
"description": description,
|
||||
"tags": tags,
|
||||
"categoryId": "20", # Gaming
|
||||
},
|
||||
"status": {
|
||||
"privacyStatus": privacy_status,
|
||||
"selfDeclaredMadeForKids": False,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
media = MediaFileUpload(str(video_path), mimetype="video/mp4", resumable=True)
|
||||
# media = MediaFileUpload(file_path, chunksize=-1, resumable=True)
|
||||
media = MediaFileUpload(str(file_path), chunksize=-1, resumable=True)
|
||||
|
||||
request = youtube.videos().insert(
|
||||
part="snippet,status",
|
||||
body=request_body,
|
||||
media_body=media
|
||||
)
|
||||
if DEBUG:
|
||||
print("🔍 DEBUGGING upload_video")
|
||||
print(f" • file_path: {file_path} ({type(file_path)})")
|
||||
print(f" • is_vertical: {is_vertical}")
|
||||
print(f" • stream_date: {stream_date}")
|
||||
print(f" • private: {private}")
|
||||
print(f" • title: {title}")
|
||||
print(f" • description: {description}")
|
||||
print(f" • tags: {tags}")
|
||||
print(f" • categoryId: {'20'} (should be int or str)")
|
||||
|
||||
response = request.execute()
|
||||
video_id = response["id"]
|
||||
return f"https://youtu.be/{video_id}"
|
||||
|
||||
request = youtube.videos().insert(
|
||||
part="snippet,status",
|
||||
body=body,
|
||||
media_body=media
|
||||
)
|
||||
|
||||
response = None
|
||||
while response is None:
|
||||
status, response = request.next_chunk()
|
||||
if status:
|
||||
print(f"🟡 Uploading: {int(status.progress() * 100)}%")
|
||||
|
||||
print(f"✅ Upload complete: https://youtu.be/{response['id']}")
|
||||
return f"https://youtu.be/{response['id']}"
|
||||
|
||||
except HttpError as e:
|
||||
print(f"❌ YouTube API error: {e}")
|
||||
return ""
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Unexpected error during upload: {e}")
|
||||
return ""
|
||||
|
||||
62
upload_youtube_montage.py
Normal file
62
upload_youtube_montage.py
Normal file
@ -0,0 +1,62 @@
|
||||
"""
|
||||
upload_montage_youtube.py
|
||||
|
||||
Standalone entry point to upload a rendered Fortnite montage video to YouTube.
|
||||
Assumes that the input video is a montage and therefore does NOT rely on a notes.* file.
|
||||
|
||||
Handles:
|
||||
- Validating input parameters (video path)
|
||||
- Deriving vertical format from filename
|
||||
- Generating dynamic description via OpenAI
|
||||
- Uploading to YouTube with appropriate metadata
|
||||
- Flagging video as private if DEBUG is enabled
|
||||
|
||||
Author: Llama Chile Shop
|
||||
Created: 2025-07-22
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from modules.config import DEBUG
|
||||
from modules.yt_poster import upload_video
|
||||
from modules.description_utils import generate_montage_description
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Entry point to handle YouTube upload of montage video.
|
||||
Usage:
|
||||
python upload_montage_youtube.py <video_path>
|
||||
"""
|
||||
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python upload_montage_youtube.py <path_to_rendered_video>")
|
||||
sys.exit(1)
|
||||
|
||||
# Extract stream date from parent directory (Z:\2025.06.20)
|
||||
video_path = Path(sys.argv[1])
|
||||
stream_date = video_path.parents[1].name # '2025.06.20'
|
||||
|
||||
if not os.path.isfile(video_path):
|
||||
print(f"[ERROR] File not found: {video_path}")
|
||||
sys.exit(1)
|
||||
|
||||
video_name = os.path.basename(video_path)
|
||||
is_vertical = "-vert" in video_path.stem or "-vertical" in video_path.stem
|
||||
|
||||
# Generate a dynamic, humorous montage description
|
||||
description = generate_montage_description()
|
||||
|
||||
# Upload the video to YouTube
|
||||
upload_video(
|
||||
file_path=video_path,
|
||||
is_vertical=is_vertical,
|
||||
stream_date=stream_date,
|
||||
description=description,
|
||||
private=DEBUG
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user