Files
animily_music/upload_youtube.py
javamon c3f8d6b288 feat: v2 파이프라인 — 매일 2곡 다양한 장르 BGM 자동 생성/업로드
- daily_precache.py: Claude Code CLI 프리캐시 (날짜/계절/기념일 테마)
- daily_scheduler.py: ACE-Step 음악 → FLUX 이미지 → 영상 렌더 → 큐
- upload_scheduled.py: auto_shorts 동일 큐 방식 업로드
- PRECACHE_GUIDE_MUSIC.md: 19개 장르, 감성 제목, 재생목록 자유 생성
- generate_image.py: --lowvram 제거, GPU VRAM 확인 추가
- config.py: @animily-music 토큰 경로 변경

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 19:24:50 +09:00

242 lines
7.9 KiB
Python
Executable File

"""Animily Music - YouTube 업로드
독립 YouTube 업로드 모듈. 기존 auto_shorts의 토큰을 공유하되 코드는 독립.
"""
import os
import pickle
from google.auth.transport.requests import Request
from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload
from config import TOKEN_PATH, YOUTUBE_CATEGORY_ID
DESCRIPTION_TEMPLATE = """{title}
🎵 저작권 프리 음악 (Copyright Free)
본 음악은 AI로 생성된 저작권 프리 음악입니다.
개인/상업적 용도 모두 자유롭게 사용하실 수 있습니다.
출처 표기 없이 자유롭게 사용 가능합니다.
🎵 Copyright Free Music
This music is AI-generated and copyright-free.
Free to use for any personal or commercial purpose.
No attribution required.
━━━━━━━━━━━━━━━━━━━━━━━━
🔬 과학적 근거
• University of Glasgow + Scottish SPCA (2017): 레게/소프트록에서 개 스트레스 최소
• Through a Dog's Ear (2008): 50-60 BPM 솔로 피아노에서 70%+ 진정 반응
• Snowdon & Teie (2015): 고양이는 퍼링 주파수(25-50Hz)에 긍정 반응
━━━━━━━━━━━━━━━━━━━━━━━━
🐾 반려동물 행동교정 플랫폼 애니밀리
📲 앱 다운로드: 앱스토어/구글플레이에서 '애니밀리' 검색
🌐 https://conimals.co.kr/
🤖 AI 생성 음악 (ACE-Step 1.5)
#반려동물음악 #강아지수면음악 #고양이음악 #펫힐링 #저작권프리
#dogmusic #catmusic #petrelaxation #copyrightfree #royaltyfree"""
FIRST_COMMENT = """🎵 저작권 프리 음악입니다!
개인/상업적 용도 모두 자유롭게 사용하세요.
출처 표기 불필요, 다운로드 자유 ✅
🎵 This is copyright-free music!
Free for personal and commercial use.
No attribution needed ✅"""
DEFAULT_TAGS = [
"반려동물음악", "강아지수면음악", "고양이음악", "펫힐링",
"저작권프리", "수면음악", "분리불안", "반려동물",
"dog music", "cat music", "pet relaxation", "copyright free",
"royalty free", "sleep music", "calming music", "애니밀리",
]
def _get_youtube_client():
"""YouTube API 클라이언트 (토큰 자동 리프레시)"""
with open(TOKEN_PATH, "rb") as f:
creds = pickle.load(f)
if creds.expired and creds.refresh_token:
creds.refresh(Request())
with open(TOKEN_PATH, "wb") as f:
pickle.dump(creds, f)
print(" [YT] 토큰 리프레시 완료", flush=True)
return build("youtube", "v3", credentials=creds)
def upload_video(video_path, title, thumbnail_path=None, extra_tags=None, privacy="public"):
"""YouTube 영상 업로드
Args:
video_path: 영상 파일 경로
title: 영상 제목
thumbnail_path: 썸네일 이미지 경로
extra_tags: 추가 태그
privacy: "public", "unlisted", "private"
Returns:
str: video_id or None
"""
youtube = _get_youtube_client()
description = DESCRIPTION_TEMPLATE.format(title=title)
tags = list(DEFAULT_TAGS)
if extra_tags:
tags.extend(extra_tags)
body = {
"snippet": {
"title": title[:100],
"description": description,
"tags": tags[:30],
"categoryId": YOUTUBE_CATEGORY_ID,
"defaultLanguage": "ko",
},
"status": {
"privacyStatus": privacy,
"madeForKids": False,
"selfDeclaredMadeForKids": False,
},
}
print(f" [YT] 업로드 시작: {title}", flush=True)
print(f" [YT] 파일: {video_path} ({os.path.getsize(video_path) / (1024*1024):.0f}MB)", flush=True)
media = MediaFileUpload(
video_path,
mimetype="video/mp4",
resumable=True,
chunksize=50 * 1024 * 1024, # 50MB chunks
)
request = youtube.videos().insert(
part="snippet,status",
body=body,
media_body=media,
)
video_id = None
response = None
while response is None:
status, response = request.next_chunk()
if status:
progress = int(status.progress() * 100)
if progress % 20 == 0:
print(f" [YT] 업로드 진행: {progress}%", flush=True)
video_id = response.get("id")
print(f" [YT] 업로드 완료: https://youtu.be/{video_id}", flush=True)
# 썸네일 설정
if thumbnail_path and os.path.exists(thumbnail_path):
try:
# 2MB 제한 체크
if os.path.getsize(thumbnail_path) > 2 * 1024 * 1024:
from PIL import Image
img = Image.open(thumbnail_path).convert("RGB")
compressed = thumbnail_path.replace(".png", "_thumb.jpg")
img.save(compressed, "JPEG", quality=85)
thumbnail_path = compressed
youtube.thumbnails().set(
videoId=video_id,
media_body=MediaFileUpload(thumbnail_path, mimetype="image/jpeg"),
).execute()
print(f" [YT] 썸네일 설정 완료", flush=True)
except Exception as e:
print(f" [YT] 썸네일 실패: {e}", flush=True)
# 고정 댓글
if video_id:
try:
youtube.commentThreads().insert(
part="snippet",
body={
"snippet": {
"videoId": video_id,
"topLevelComment": {
"snippet": {"textOriginal": FIRST_COMMENT}
},
}
},
).execute()
print(f" [YT] 고정 댓글 등록 완료", flush=True)
except Exception as e:
print(f" [YT] 고정 댓글 실패: {e}", flush=True)
return video_id
# 재생목록 캐시
_PLAYLIST_CACHE = {}
def create_or_get_playlist(title="반려동물 음악"):
"""재생목록 ID 반환 (없으면 자동 생성)"""
if title in _PLAYLIST_CACHE:
return _PLAYLIST_CACHE[title]
youtube = _get_youtube_client()
# 기존 재생목록 검색
try:
pl = youtube.playlists().list(part="snippet", mine=True, maxResults=50).execute()
for item in pl.get("items", []):
if item["snippet"]["title"] == title:
_PLAYLIST_CACHE[title] = item["id"]
print(f" [YT] 재생목록 찾음: {title} (id={item['id']})", flush=True)
return item["id"]
except Exception as e:
print(f" [YT] 재생목록 검색 실패: {e}", flush=True)
# 없으면 생성
try:
body = {
"snippet": {
"title": title,
"description": "AI 생성 반려동물 음악 | 저작권 프리 | Copyright Free Pet Music",
},
"status": {"privacyStatus": "public"},
}
resp = youtube.playlists().insert(part="snippet,status", body=body).execute()
pl_id = resp["id"]
_PLAYLIST_CACHE[title] = pl_id
print(f" [YT] 재생목록 생성: {title} (id={pl_id})", flush=True)
return pl_id
except Exception as e:
print(f" [YT] 재생목록 생성 실패: {e}", flush=True)
return None
def add_to_playlist(video_id, playlist_id):
"""영상을 재생목록에 추가"""
if not playlist_id or not video_id:
return
youtube = _get_youtube_client()
try:
youtube.playlistItems().insert(
part="snippet",
body={
"snippet": {
"playlistId": playlist_id,
"resourceId": {
"kind": "youtube#video",
"videoId": video_id,
},
}
},
).execute()
print(f" [YT] 재생목록 추가 완료", flush=True)
except Exception as e:
print(f" [YT] 재생목록 추가 실패: {e}", flush=True)
if __name__ == "__main__":
print("upload_youtube.py - scheduler.py를 통해 사용하세요")