초기 프로젝트 구성: 반려동물 음악 롱폼 자동 생성 파이프라인

- ACE-Step 1.5 음악 생성 (과학적 근거 기반)
- FLUX 이미지 생성 (신카이 마코토 스타일)
- ffmpeg 영상 렌더링 (워터마크 포함)
- YouTube Data API 롱폼 업로드
- 프롬프트 및 문서 포함
This commit is contained in:
javamon
2026-04-21 15:41:20 +09:00
commit 37d13be48d
13 changed files with 1777 additions and 0 deletions

209
upload_youtube.py Executable file
View 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를 통해 사용하세요")