초기 프로젝트 구성: 반려동물 음악 롱폼 자동 생성 파이프라인
- ACE-Step 1.5 음악 생성 (과학적 근거 기반) - FLUX 이미지 생성 (신카이 마코토 스타일) - ffmpeg 영상 렌더링 (워터마크 포함) - YouTube Data API 롱폼 업로드 - 프롬프트 및 문서 포함
This commit is contained in:
291
scheduler.py
Executable file
291
scheduler.py
Executable file
@@ -0,0 +1,291 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user