Files
animily_music/scheduler.py
javamon 37d13be48d 초기 프로젝트 구성: 반려동물 음악 롱폼 자동 생성 파이프라인
- ACE-Step 1.5 음악 생성 (과학적 근거 기반)
- FLUX 이미지 생성 (신카이 마코토 스타일)
- ffmpeg 영상 렌더링 (워터마크 포함)
- YouTube Data API 롱폼 업로드
- 프롬프트 및 문서 포함
2026-04-21 15:41:20 +09:00

292 lines
11 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Animily Music - 메인 스케줄러
1시간 / 2시간 / 12시간(1h+2h 혼합×4) 영상 생성 → 유튜브 업로드.
GPU 메모리 관리: ACE-Step → 종료 → FLUX → 종료 → ffmpeg.
"""
import gc
import os
import signal
import subprocess
import sys
import time
from datetime import datetime
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from config import OUTPUT_DIR, LOG_DIR, SEGMENTS_FOR_1H, SEGMENTS_FOR_2H
from generate_music import generate_2h_music, _wait_for_acestep
from generate_image import generate_image, load_image_prompt, _stop_comfyui, generate_thumbnail
from render_video import render_video, concat_videos
from upload_youtube import upload_video, create_or_get_playlist, add_to_playlist
def _kill_acestep():
"""ACE-Step 서버 관련 프로세스 GPU 메모리 해제 확인"""
# ACE-Step은 외부 서버이므로 직접 종료하지 않음
# 다만 GPU 메모리가 해제될 때까지 대기
print(" [GPU] ACE-Step 작업 완료, GPU 메모리 해제 대기...", flush=True)
time.sleep(10)
gc.collect()
def _cleanup_outputs(*paths):
"""임시 파일 정리"""
for p in paths:
if p and os.path.exists(p):
os.remove(p)
print(f" [CLEAN] 삭제: {os.path.basename(p)}", flush=True)
def generate_1h_music(animal_type="dog", style_index=0):
"""1시간 음악 생성"""
from generate_music import (
_load_prompts, _submit_task, _poll_result,
_download_audio, _crossfade_segments,
)
from config import SEGMENT_DURATION, CROSSFADE_SEC
prompts = _load_prompts(animal_type)
style = prompts["styles"][style_index % len(prompts["styles"])]
base_caption = style["caption"]
bpm = style["bpm"]
keyscale = style["keyscale"]
variations = style.get("variations", [])
segments_needed = SEGMENTS_FOR_1H # 12 * 295s ≈ 59분
print(f"\n{'='*60}")
print(f"[MUSIC] {animal_type} 1시간 음악 생성 ({segments_needed}세그먼트)")
print(f"{'='*60}\n")
segment_paths = []
for i in range(segments_needed):
seg_path = os.path.join(OUTPUT_DIR, f"seg_1h_{animal_type}_{i:02d}.wav")
if variations:
var = variations[i % len(variations)]
caption = f"{base_caption} {var}"
else:
caption = base_caption
print(f" [SEG {i+1}/{segments_needed}] 생성 중...", flush=True)
t0 = time.time()
try:
task_id = _submit_task(caption, bpm, keyscale, SEGMENT_DURATION)
audio_info = _poll_result(task_id, timeout=900)
_download_audio(audio_info, seg_path)
print(f" [SEG {i+1}/{segments_needed}] 완료 ({time.time()-t0:.0f}초)")
segment_paths.append(seg_path)
except Exception as e:
print(f" [SEG {i+1}] 실패: {e}")
continue
if len(segment_paths) < 6:
raise RuntimeError(f"1시간 음악: {len(segment_paths)}개만 성공 — 최소 6개 필요")
output_path = os.path.join(OUTPUT_DIR, f"music_{animal_type}_1h.wav")
_crossfade_segments(segment_paths, output_path, CROSSFADE_SEC * 1000)
for p in segment_paths:
if os.path.exists(p):
os.remove(p)
return output_path
def run_pipeline():
"""전체 파이프라인 실행
생성할 영상:
1. 1시간짜리 (강아지 솔로 피아노)
2. 2시간짜리 (강아지 소프트 레게)
3. 12시간짜리 = (1시간 + 2시간) × 4 반복
"""
start_time = time.time()
today = datetime.now().strftime("%Y%m%d")
print(f"\n{'#'*60}")
print(f"# Animily Music Pipeline - {today}")
print(f"{'#'*60}\n")
results = []
video_1h_path = None
video_2h_path = None
# ============================================================
# STEP 1: ACE-Step 음악 생성 (1시간 + 2시간)
# ============================================================
print("\n[STEP 1] 음악 생성 (ACE-Step)")
print("=" * 40)
if not _wait_for_acestep(timeout=30):
# ACE-Step 서버 시작 시도
print(" [MUSIC] ACE-Step 서버 시작 중...", flush=True)
subprocess.Popen(
["bash", "-c",
"cd /home/javamon/ACE-Step-1.5 && source venv/bin/activate && "
"python -m uvicorn acestep.api_server:app --host 0.0.0.0 --port 8001 --workers 1"],
stdout=open(os.path.join(LOG_DIR, "acestep.log"), "a"),
stderr=open(os.path.join(LOG_DIR, "acestep.log"), "a"),
)
if not _wait_for_acestep(timeout=180):
print(" [ERROR] ACE-Step 서버 시작 실패!", flush=True)
return
# 1시간 음악 (강아지 - 솔로 피아노, style_index=0)
music_1h = generate_1h_music("dog", style_index=0)
print(f" [MUSIC] 1시간 음악 완료: {music_1h}")
# 2시간 음악 (강아지 - 소프트 레게, style_index=1)
music_2h = generate_2h_music("dog", style_index=1)
print(f" [MUSIC] 2시간 음악 완료: {music_2h}")
# ACE-Step 작업 완료 → GPU 해제 대기
_kill_acestep()
# ============================================================
# STEP 2: FLUX 이미지 생성 (영상당 1장)
# ============================================================
print("\n[STEP 2] 이미지 생성 (FLUX)")
print("=" * 40)
image_1h = os.path.join(OUTPUT_DIR, f"image_1h_{today}.png")
image_2h = os.path.join(OUTPUT_DIR, f"image_2h_{today}.png")
thumb_1h = os.path.join(OUTPUT_DIR, f"thumb_1h_{today}.jpg")
thumb_2h = os.path.join(OUTPUT_DIR, f"thumb_2h_{today}.jpg")
# 1시간용 이미지
prompt_1h = load_image_prompt("dog")
if not generate_image(prompt_1h, image_1h):
print(" [WARN] 1시간 이미지 생성 실패, 2시간 이미지 공유", flush=True)
# 2시간용 이미지 (다른 프롬프트)
prompt_2h = load_image_prompt("dog")
if not generate_image(prompt_2h, image_2h):
# 1시간 이미지를 재사용
if os.path.exists(image_1h):
subprocess.run(["cp", image_1h, image_2h])
# 썸네일 생성
if os.path.exists(image_1h):
generate_thumbnail(image_1h, thumb_1h)
if os.path.exists(image_2h):
generate_thumbnail(image_2h, thumb_2h)
# FLUX/ComfyUI 종료 → GPU 메모리 해제
_stop_comfyui()
# ============================================================
# STEP 3: 영상 렌더링 (ffmpeg)
# ============================================================
print("\n[STEP 3] 영상 렌더링")
print("=" * 40)
# 사용할 이미지 결정
img_for_1h = image_1h if os.path.exists(image_1h) else image_2h
img_for_2h = image_2h if os.path.exists(image_2h) else image_1h
# 1시간 영상
video_1h_path = os.path.join(OUTPUT_DIR, f"pet_music_1h_{today}.mp4")
if render_video(img_for_1h, music_1h, video_1h_path):
print(f" [VIDEO] 1시간 영상 완료")
else:
print(f" [ERROR] 1시간 영상 렌더 실패!")
# 2시간 영상
video_2h_path = os.path.join(OUTPUT_DIR, f"pet_music_2h_{today}.mp4")
if render_video(img_for_2h, music_2h, video_2h_path):
print(f" [VIDEO] 2시간 영상 완료")
else:
print(f" [ERROR] 2시간 영상 렌더 실패!")
# 12시간 영상 = (1h + 2h) × 4 반복
video_12h_path = os.path.join(OUTPUT_DIR, f"pet_music_12h_{today}.mp4")
if video_1h_path and video_2h_path and os.path.exists(video_1h_path) and os.path.exists(video_2h_path):
# 1h + 2h = 3h, × 4 = 12h
mix_list = [video_1h_path, video_2h_path] * 4
if concat_videos(mix_list, video_12h_path):
print(f" [VIDEO] 12시간 영상 완료")
else:
print(f" [ERROR] 12시간 영상 렌더 실패!")
# ============================================================
# STEP 4: YouTube 업로드
# ============================================================
print("\n[STEP 4] YouTube 업로드")
print("=" * 40)
playlist_id = create_or_get_playlist()
# 1시간 업로드
if video_1h_path and os.path.exists(video_1h_path):
title_1h = "🐕 강아지가 좋아하는 음악 1시간 | 솔로 피아노 수면음악 [과학적 검증]"
vid_id = upload_video(
video_1h_path, title_1h,
thumbnail_path=thumb_1h if os.path.exists(thumb_1h) else None,
extra_tags=["1시간", "솔로피아노", "수면"],
)
if vid_id and playlist_id:
add_to_playlist(vid_id, playlist_id)
results.append(("1시간", vid_id))
# 2시간 업로드
if video_2h_path and os.path.exists(video_2h_path):
title_2h = "🐕 강아지가 좋아하는 음악 2시간 | 소프트 레게, 분리불안 완화 [과학적 검증]"
vid_id = upload_video(
video_2h_path, title_2h,
thumbnail_path=thumb_2h if os.path.exists(thumb_2h) else None,
extra_tags=["2시간", "레게", "분리불안"],
)
if vid_id and playlist_id:
add_to_playlist(vid_id, playlist_id)
results.append(("2시간", vid_id))
# 12시간 업로드
if os.path.exists(video_12h_path):
title_12h = "🐕 강아지가 좋아하는 음악 12시간 | 수면, 분리불안, 스트레스 해소 [과학적 검증]"
vid_id = upload_video(
video_12h_path, title_12h,
thumbnail_path=thumb_2h if os.path.exists(thumb_2h) else None,
extra_tags=["12시간", "수면음악", "분리불안", "장시간"],
)
if vid_id and playlist_id:
add_to_playlist(vid_id, playlist_id)
results.append(("12시간", vid_id))
# ============================================================
# STEP 5: 정리
# ============================================================
print("\n[STEP 5] 정리")
print("=" * 40)
# 임시 음악 파일 삭제 (영상에 포함됐으므로)
_cleanup_outputs(music_1h, music_2h, image_1h, image_2h, thumb_1h, thumb_2h)
# 영상 파일은 업로드 확인 후 삭제
for label, vid_id in results:
if vid_id:
print(f" [{label}] 업로드 성공 → 영상 파일 삭제")
# 업로드 성공한 영상만 삭제
if results:
for path in [video_1h_path, video_2h_path, video_12h_path]:
if path and os.path.exists(path):
os.remove(path)
print(f" [CLEAN] {os.path.basename(path)} 삭제")
# GPU 메모리 최종 해제 확인
gc.collect()
elapsed = time.time() - start_time
print(f"\n{'#'*60}")
print(f"# 파이프라인 완료 ({elapsed/60:.1f}분)")
print(f"# 결과:")
for label, vid_id in results:
url = f"https://youtu.be/{vid_id}" if vid_id else "실패"
print(f"# {label}: {url}")
print(f"{'#'*60}\n")
if __name__ == "__main__":
run_pipeline()