초기 프로젝트 구성: 반려동물 음악 롱폼 자동 생성 파이프라인
- ACE-Step 1.5 음악 생성 (과학적 근거 기반) - FLUX 이미지 생성 (신카이 마코토 스타일) - ffmpeg 영상 렌더링 (워터마크 포함) - YouTube Data API 롱폼 업로드 - 프롬프트 및 문서 포함
This commit is contained in:
209
upload_youtube.py
Executable file
209
upload_youtube.py
Executable file
@@ -0,0 +1,209 @@
|
||||
"""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_ID = "PLr8dPYZT-hCUjL-OgPxJdF81Dvn_g8Vbg"
|
||||
|
||||
|
||||
def create_or_get_playlist(title="뮤직큐우"):
|
||||
"""재생목록 ID 반환 (기존 '뮤직큐우' 사용)"""
|
||||
return PLAYLIST_ID
|
||||
|
||||
|
||||
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를 통해 사용하세요")
|
||||
Reference in New Issue
Block a user