Initial YouTube description generation and authentication — work in progress

This commit is contained in:
2025-07-23 20:28:20 -07:00
parent 6c5850b1aa
commit 2f6740eb54
5 changed files with 221 additions and 76 deletions

View File

@ -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

View 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 doesnt 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

View File

@ -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}"

View File

@ -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 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)
return build("youtube", "v3", credentials=creds)
if not description:
description = str(generate_montage_description())
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")
# Construct tags and privacy status
tags = ["Fortnite", "Zero Build", "Solo", "Gramps", "CoolHandGramps"]
privacy_status = "private" if private else "public"
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. 🎮"
)
# Authenticate
from authorize_youtube import get_authenticated_service
youtube = get_authenticated_service()
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 = {
body = {
"snippet": {
"title": title,
"description": description,
"tags": ["Fortnite", "Gaming", "Senior Gamer", "LlamaChileShop"],
"tags": tags,
"categoryId": "20", # Gaming
},
"status": {
"privacyStatus": "private",
"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)
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)")
request = youtube.videos().insert(
part="snippet,status",
body=request_body,
body=body,
media_body=media
)
response = request.execute()
video_id = response["id"]
return f"https://youtu.be/{video_id}"
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
View 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()