- ACE-Step 1.5 음악 생성 (과학적 근거 기반) - FLUX 이미지 생성 (신카이 마코토 스타일) - ffmpeg 영상 렌더링 (워터마크 포함) - YouTube Data API 롱폼 업로드 - 프롬프트 및 문서 포함
210 lines
6.7 KiB
Python
Executable File
210 lines
6.7 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_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를 통해 사용하세요")
|