[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
# 📜 2026-05-08 회사 대화록
|
||||
|
||||
_모든 명령·분배·산출물·대화가 시간순으로 누적됩니다. 두뇌가 자동 인덱싱·동기화합니다._
|
||||
|
||||
## [00:11:45] 📱 **사용자(텔레그램)**
|
||||
|
||||
안녕
|
||||
|
||||
## [00:11:58] 💬 **비서** · _텔레그램 응답_
|
||||
|
||||
*안녕하세요. 무엇을 도와드릴까요?*
|
||||
|
||||
## [00:16:10] 📱 **사용자(텔레그램)**
|
||||
|
||||
너 이름 뭐야?
|
||||
|
||||
## [00:16:17] 💬 **비서** · _텔레그램 응답_
|
||||
|
||||
*저는 G1nation의 비서입니다. 무엇을 도와드릴까요?*
|
||||
|
||||
## [00:16:36] 📱 **사용자(텔레그램)**
|
||||
|
||||
아 넌 g1nation의 비서구나
|
||||
|
||||
## [00:16:41] 💬 **비서** · _텔레그램 응답_
|
||||
|
||||
*네, 맞습니다. G1nation의 비서입니다. 필요한 업무가 있으시면 언제든지 말씀해주세요.*
|
||||
|
||||
## [00:17:42] 📱 **사용자(텔레그램)**
|
||||
|
||||
rxrx 주가 얼마야? 지금 시간 기준
|
||||
|
||||
## [00:17:48] 💬 **비서** · _텔레그램 응답_
|
||||
|
||||
*죄송합니다. 저는 실시간 주가 정보 조회 기능은 제공하지 않습니다. 다른 업무 관련 문의사항이 있으시면 말씀해주세요.*
|
||||
|
||||
## [00:18:03] 📱 **사용자(텔레그램)**
|
||||
|
||||
그렇구나. 그럼 우리 아이들은 지금 뭐해?
|
||||
|
||||
## [00:18:12] 💬 **비서** · _텔레그램 응답_
|
||||
|
||||
*죄송합니다. 제가 현재 접근할 수 있는 업무 관련 정보나 일정 외에는 해당 내용을 확인할 수 없습니다. 업무 관련 문의사항이 있으시면 말씀해주세요.*
|
||||
|
||||
## [00:18:25] 📱 **사용자(텔레그램)**
|
||||
|
||||
오늘 회의가 뭐뭐 있었지?
|
||||
|
||||
## [00:43:27] 📱 **사용자(텔레그램)**
|
||||
|
||||
hi
|
||||
|
||||
## [00:43:33] 💬 **비서** · _텔레그램 응답_
|
||||
|
||||
*안녕하세요. G1nation의 비서입니다. 어떤 업무로 도와드릴까요?*
|
||||
|
||||
## [00:51:10] 📱 **사용자(텔레그램)**
|
||||
|
||||
안녕
|
||||
|
||||
## [00:51:15] 💬 **비서** · _텔레그램 응답_
|
||||
|
||||
*안녕하세요. G1nation의 비서입니다. 어떤 업무로 도와드릴까요?*
|
||||
@@ -0,0 +1,7 @@
|
||||
# 📜 2026-05-09 회사 대화록
|
||||
|
||||
_모든 명령·분배·산출물·대화가 시간순으로 누적됩니다. 두뇌가 자동 인덱싱·동기화합니다._
|
||||
|
||||
## [09:02:44] 📱 **사용자(텔레그램)**
|
||||
|
||||
내 컴퓨터 조작 개능해? 지금 vs code 화면 찍어 보내줘
|
||||
@@ -0,0 +1,32 @@
|
||||
<!-- version: music_v4 -->
|
||||
# 🎵 BGM 생성 — ACE-Step
|
||||
|
||||
영상에 어울리는 BGM을 텍스트 프롬프트로 생성. ACE-Step 1.5 로컬 모델 사용.
|
||||
|
||||
## 사용 전 체크
|
||||
- `music_studio_setup.py` 가 먼저 실행돼야 함 (한 번만)
|
||||
- 첫 BGM 생성 시 모델 weight 다운로드 (~10GB, 인터넷 필요)
|
||||
- 이후엔 100% 오프라인
|
||||
|
||||
## 설정 (⚙️ 클릭해서 변경)
|
||||
- `PROMPT` — 음악 묘사 (영어가 모델에 더 잘 듣음). 기본: 차분한 한국 유튜브 인트로
|
||||
- `DURATION_SEC` — 길이 초 (기본 30)
|
||||
- `GENRE` — 장르 힌트 (lo-fi, ambient, cinematic, edm 등)
|
||||
- `OUTPUT_DIR` — 저장 위치 (기본 ~/connect-ai-music/output/)
|
||||
|
||||
## 출력
|
||||
- MP3 파일 (~/connect-ai-music/output/bgm_<timestamp>.mp3)
|
||||
- 다음 단계 도구(`music_to_video.py`)가 자동으로 이 파일 사용
|
||||
|
||||
## 좋은 프롬프트 팁
|
||||
- ✓ "calm intro music, soft piano, 90 BPM, hopeful mood"
|
||||
- ✓ "energetic synth lead, cyberpunk, fast tempo, electronic drums"
|
||||
- ✗ "음악" (너무 추상)
|
||||
|
||||
## 첫 실행 시간
|
||||
- 모델 다운로드: 5~30분 (인터넷 속도)
|
||||
- 30초 BGM 생성: 30~120초 (Mac M1/M2/M3/M5 기준)
|
||||
- 두 번째부터는 다운로드 없이 바로
|
||||
|
||||
## 비용
|
||||
완전 무료, 오프라인. API 키 X.
|
||||
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env python3
|
||||
# version: music_v4
|
||||
"""BGM 생성 — 설치된 모델에 따라 자동 dispatch.
|
||||
|
||||
music_studio_setup.py 로 설치한 모델(MusicGen / ACE-Step)을 자동 감지해서
|
||||
같은 인터페이스로 BGM 생성. 사용자는 모델 차이 신경 쓸 필요 X.
|
||||
|
||||
config:
|
||||
PROMPT — 음악 묘사 (영어 권장)
|
||||
DURATION_SEC — 길이 (초)
|
||||
GENRE — 장르 힌트 (lo-fi, ambient, cinematic, edm 등)
|
||||
OUTPUT_DIR — 저장 위치 (디폴트 ~/connect-ai-music/output/)
|
||||
"""
|
||||
import os, sys, json, subprocess, time
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
SETUP_CONFIG = os.path.join(HERE, "music_studio_setup.json")
|
||||
GEN_CONFIG = os.path.join(HERE, "music_generate.json")
|
||||
|
||||
|
||||
def _log(msg, kind="info"):
|
||||
prefix = {"info": "🎵", "ok": "✅", "warn": "⚠️ ", "err": "❌"}.get(kind, "•")
|
||||
print(f"{prefix} {msg}", file=sys.stderr, flush=True)
|
||||
|
||||
|
||||
def _load(p):
|
||||
if os.path.exists(p):
|
||||
try:
|
||||
with open(p, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
pass
|
||||
return {}
|
||||
|
||||
|
||||
def _generate_musicgen(setup, prompt, duration_sec, output_path):
|
||||
"""MusicGen 류 (transformers 기반). 가벼움."""
|
||||
venv_python = setup.get("VENV_PYTHON")
|
||||
hf_id = setup.get("HF_ID", "facebook/musicgen-small")
|
||||
|
||||
# MusicGen은 약 50 토큰/초 (sample rate 32000Hz, 50hz token rate)
|
||||
# duration → max_new_tokens 환산
|
||||
max_tokens = max(64, int(duration_sec * 50))
|
||||
|
||||
# v2.89.76 — outer f-string이 prompt!r 치환할 때 quote 충돌하던 문제 수정.
|
||||
# 변수에 먼저 담은 뒤 inner f-string에서 {{변수}} 형태로 참조 (literal { 이스케이프).
|
||||
wav_path = output_path.replace('.mp3', '.wav')
|
||||
script = f"""
|
||||
import os, sys
|
||||
os.environ['TRANSFORMERS_VERBOSITY'] = 'error'
|
||||
import logging, warnings
|
||||
logging.getLogger('transformers').setLevel(logging.ERROR)
|
||||
warnings.filterwarnings('ignore')
|
||||
import torch, scipy.io.wavfile
|
||||
|
||||
PROMPT = {prompt!r}
|
||||
HF_ID = {hf_id!r}
|
||||
WAV_PATH = {wav_path!r}
|
||||
DURATION_SEC = {duration_sec}
|
||||
MAX_TOKENS = {max_tokens}
|
||||
|
||||
print('🔧 모델 로드 중...', file=sys.stderr, flush=True)
|
||||
from transformers import MusicgenForConditionalGeneration, AutoProcessor
|
||||
processor = AutoProcessor.from_pretrained(HF_ID)
|
||||
model = MusicgenForConditionalGeneration.from_pretrained(HF_ID)
|
||||
device = 'mps' if torch.backends.mps.is_available() else ('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
model = model.to(device)
|
||||
print('🎵 디바이스: ' + str(device), file=sys.stderr, flush=True)
|
||||
print('🎼 생성 중... (' + str(DURATION_SEC) + '초)', file=sys.stderr, flush=True)
|
||||
inputs = processor(text=[PROMPT], padding=True, return_tensors='pt').to(device)
|
||||
audio = model.generate(**inputs, max_new_tokens=MAX_TOKENS)
|
||||
audio_np = audio[0, 0].cpu().numpy()
|
||||
sr = model.config.audio_encoder.sampling_rate
|
||||
scipy.io.wavfile.write(WAV_PATH, sr, audio_np)
|
||||
print('✅ wav: ' + WAV_PATH, file=sys.stderr, flush=True)
|
||||
"""
|
||||
proc = subprocess.run([venv_python, "-c", script], capture_output=True, text=True)
|
||||
if proc.stderr.strip():
|
||||
for line in proc.stderr.splitlines():
|
||||
_log(f" {line}")
|
||||
if proc.returncode != 0:
|
||||
return False, f"MusicGen 추론 실패 (exit {proc.returncode})"
|
||||
|
||||
wav_path = output_path.replace('.mp3', '.wav')
|
||||
if not os.path.exists(wav_path):
|
||||
return False, "wav 파일 생성 안 됨"
|
||||
|
||||
# wav → mp3 변환 (ffmpeg 있을 때)
|
||||
if subprocess.run(["which", "ffmpeg"], capture_output=True).returncode == 0:
|
||||
subprocess.run([
|
||||
"ffmpeg", "-y", "-i", wav_path, "-codec:a", "libmp3lame", "-qscale:a", "2", output_path
|
||||
], capture_output=True)
|
||||
if os.path.exists(output_path):
|
||||
os.remove(wav_path) # mp3로 변환했으니 wav는 삭제
|
||||
return True, output_path
|
||||
# ffmpeg 없으면 wav 그대로
|
||||
return True, wav_path
|
||||
|
||||
|
||||
def _generate_acestep(setup, prompt, duration_sec, output_path):
|
||||
"""ACE-Step — repo의 infer 스크립트 호출. 무거움."""
|
||||
venv_python = setup.get("VENV_PYTHON")
|
||||
repo_dir = setup.get("ACE_STEP_DIR")
|
||||
|
||||
# ACE-Step entry point 자동 탐색
|
||||
candidates = ["infer.py", "src/infer.py", "scripts/infer.py", "ace_step/infer.py", "main.py"]
|
||||
infer_script = None
|
||||
for c in candidates:
|
||||
full = os.path.join(repo_dir, c)
|
||||
if os.path.exists(full):
|
||||
infer_script = full
|
||||
break
|
||||
if not infer_script:
|
||||
return False, f"ACE-Step infer 스크립트 못 찾음 — {repo_dir} 의 README 확인 필요"
|
||||
|
||||
cmd = [venv_python, infer_script,
|
||||
"--prompt", prompt, "--duration", str(duration_sec), "--output", output_path]
|
||||
proc = subprocess.run(cmd, cwd=repo_dir, capture_output=True, text=True)
|
||||
if proc.stderr.strip():
|
||||
for line in proc.stderr.splitlines()[-30:]:
|
||||
_log(f" {line}")
|
||||
if proc.returncode != 0:
|
||||
return False, f"ACE-Step 실패 (exit {proc.returncode}). README의 명령 형식 확인 필요"
|
||||
if not os.path.exists(output_path):
|
||||
return False, "출력 파일 없음 — ACE-Step 명령 형식이 다를 수 있음"
|
||||
return True, output_path
|
||||
|
||||
|
||||
def main():
|
||||
setup = _load(SETUP_CONFIG)
|
||||
if not setup.get("INSTALLED_AT"):
|
||||
print("❌ 음악 모델 미설치.")
|
||||
print(" 먼저 같은 폴더의 'music_studio_setup.py' 실행해주세요 (▶ 클릭).")
|
||||
print(" 기본은 MusicGen Small (300MB) — 가벼움.")
|
||||
sys.exit(1)
|
||||
|
||||
venv_python = setup.get("VENV_PYTHON")
|
||||
if not (venv_python and os.path.exists(venv_python)):
|
||||
print("❌ 설치 정보 손상. music_studio_setup.py 다시 실행해주세요.")
|
||||
sys.exit(1)
|
||||
|
||||
cfg = _load(GEN_CONFIG)
|
||||
prompt = (cfg.get("PROMPT") or "calm korean YouTube intro music, gentle piano, hopeful").strip()
|
||||
duration = int(cfg.get("DURATION_SEC") or 30)
|
||||
genre = (cfg.get("GENRE") or "").strip()
|
||||
if genre:
|
||||
prompt = f"{prompt}, genre: {genre}"
|
||||
|
||||
output_dir = cfg.get("OUTPUT_DIR") or os.path.expanduser("~/connect-ai-music/output")
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
||||
output_path = os.path.join(output_dir, f"bgm_{timestamp}.mp3")
|
||||
|
||||
model_label = setup.get("INSTALLED_MODEL", "unknown")
|
||||
_log(f"모델: {model_label}")
|
||||
_log(f"프롬프트: {prompt}")
|
||||
_log(f"길이: {duration}초")
|
||||
_log(f"출력: {output_path}")
|
||||
|
||||
install_kind = setup.get("INSTALL_KIND", "transformers")
|
||||
if install_kind == "transformers":
|
||||
ok, result = _generate_musicgen(setup, prompt, duration, output_path)
|
||||
elif install_kind == "acestep":
|
||||
ok, result = _generate_acestep(setup, prompt, duration, output_path)
|
||||
else:
|
||||
print(f"❌ 알 수 없는 INSTALL_KIND: {install_kind}")
|
||||
sys.exit(1)
|
||||
|
||||
if not ok:
|
||||
print(f"❌ {result}")
|
||||
sys.exit(1)
|
||||
|
||||
final_path = result
|
||||
file_size = os.path.getsize(final_path)
|
||||
print(f"✅ BGM 생성 완료")
|
||||
print(f" 🎵 모델: {model_label}")
|
||||
print(f" 📁 {final_path}")
|
||||
print(f" 📊 {file_size // 1024} KB · {duration}초")
|
||||
print(f" 💬 프롬프트: {prompt}")
|
||||
print(f" 🎬 영상에 합치려면: 같은 폴더의 'music_to_video.py' 실행")
|
||||
|
||||
# 다음 도구가 자동으로 사용
|
||||
cfg["LAST_OUTPUT"] = final_path
|
||||
cfg["LAST_PROMPT"] = prompt
|
||||
with open(GEN_CONFIG, "w", encoding="utf-8") as f:
|
||||
json.dump(cfg, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,52 @@
|
||||
<!-- version: music_v5 -->
|
||||
# 🎵 음악 스튜디오 설치 — 모델 선택 가능
|
||||
|
||||
영상 BGM을 직접 생성하는 음악 모델 설치. 5개 모델 중 본인 머신에 맞는 거 선택.
|
||||
|
||||
## 모델 비교
|
||||
|
||||
| 모델 | 디스크 | RAM | 추천 | 품질 |
|
||||
|---|---|---|---|---|
|
||||
| **musicgen-small** ⭐ 기본 | 300MB | 4GB+ | 누구나 | 보통 |
|
||||
| musicgen-medium | 1.5GB | 8GB+ | 8GB+ RAM | 좋음 |
|
||||
| musicgen-large | 3.3GB | 16GB+ | 16GB+ RAM | 매우 좋음 |
|
||||
| acestep-base | 10GB | 16GB+ | Mac M1+/CUDA | 우수 |
|
||||
| acestep-xl | 15GB | 24GB+ | 32GB+ 머신 | 최고 |
|
||||
|
||||
**자동 추천**: 처음 실행 시 본인 머신 RAM 측정해서 적절한 모델 자동 추천. 16GB Mac이면 medium, 32GB는 large.
|
||||
|
||||
## 사용 흐름
|
||||
1. ⚙️에서 `MODEL` 비워두고 ▶ 클릭 → RAM 기반 자동 추천 설치 (small/medium 디폴트)
|
||||
2. 또는 ⚙️에서 `MODEL: 'musicgen-large'` 같이 직접 선택 후 ▶
|
||||
3. 진행상황 채팅창 표시 (1~10분)
|
||||
4. 완료 후 `music_generate.py` 가 자동으로 이 모델 사용
|
||||
|
||||
## 모델 변경
|
||||
이미 다른 모델 설치돼있어도 ⚙️에서 `MODEL` 다른 값으로 바꾸고 ▶ 다시 실행하면 새 모델로 교체 (또는 추가 설치).
|
||||
|
||||
## 시스템 요구사항
|
||||
- **공통**: Python 3.10+, git
|
||||
- **MusicGen**: macOS/Linux/Windows. Apple Silicon은 MPS 가속 자동 사용
|
||||
- **ACE-Step**: 같음 + 더 큰 디스크/RAM
|
||||
|
||||
## 설치 위치
|
||||
디폴트 `~/connect-ai-music/`. ⚙️의 `INSTALL_DIR` 로 변경 가능 (외장 디스크 등).
|
||||
|
||||
## 비용
|
||||
100% 로컬·오프라인·무료. API 키·구독 0개.
|
||||
|
||||
## 트러블슈팅
|
||||
**"git/python3 없다"** → `brew install python git` (Mac) / python.org+git-scm.com 설치 (Win)
|
||||
|
||||
**디스크 부족** → 작은 모델로 변경 (musicgen-small 300MB)
|
||||
|
||||
**ACE-Step 다운로드가 너무 느림** → musicgen-medium (1.5GB)으로 충분히 좋은 품질 나옴
|
||||
|
||||
## 추천 시작 — 사용자별
|
||||
|
||||
| 사용자 | 추천 모델 | 이유 |
|
||||
|---|---|---|
|
||||
| 처음 써봄, 빨리 결과 보고 싶음 | musicgen-small | 30초 안에 첫 음악 |
|
||||
| 16GB Mac, 유튜브 BGM 만들고 싶음 | musicgen-medium | 품질·속도 밸런스 |
|
||||
| 32GB+ Mac, 음악 자주 씀 | musicgen-large 또는 acestep-base | 뚜렷한 품질 |
|
||||
| 음악 전공·사운드 디자이너 | acestep-xl | 최고 품질 |
|
||||
@@ -0,0 +1,303 @@
|
||||
#!/usr/bin/env python3
|
||||
# version: music_v5
|
||||
"""음악 스튜디오 — 다중 모델 지원 원클릭 설치.
|
||||
|
||||
선택 가능한 모델 (디스크·메모리·품질 트레이드오프):
|
||||
|
||||
┌────────────────────────┬────────┬───────────┬─────────────┐
|
||||
│ MODEL │ 디스크 │ 메모리 │ 추천 │
|
||||
├────────────────────────┼────────┼───────────┼─────────────┤
|
||||
│ musicgen-small (기본) │ 300MB │ 4GB+ │ 모든 기기 │
|
||||
│ musicgen-medium │ 1.5GB │ 6GB+ │ 8GB+ RAM │
|
||||
│ musicgen-large │ 3.3GB │ 12GB+ │ 16GB+ RAM │
|
||||
│ acestep-base │ 10GB │ 16GB+ │ 16GB+ Mac │
|
||||
│ acestep-xl │ 15GB+ │ 24GB+ │ 32GB+ 머신 │
|
||||
└────────────────────────┴────────┴───────────┴─────────────┘
|
||||
|
||||
기본값: musicgen-small — 300MB만 받고 30초만에 첫 음악. 모든 기기에서 안정적.
|
||||
큰 모델은 추론 시 명시 RAM의 1.5~2배 실제 압박 발생해서 자동 선택은 무조건 small.
|
||||
medium/large 쓰고 싶으면 MODEL 필드에 직접 지정.
|
||||
|
||||
⚙️ MODEL 필드를 위 5개 중 하나로 설정. 설치는 한 번에 한 모델만 (선택한 거).
|
||||
"""
|
||||
import os, sys, json, subprocess, shutil, time
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
CONFIG_PATH = os.path.join(HERE, "music_studio_setup.json")
|
||||
|
||||
# 모델 메타 — 디스크·RAM 추천·HuggingFace 경로·설치 방식
|
||||
MODELS = {
|
||||
"musicgen-small": {
|
||||
"disk_gb": 0.3, "ram_gb": 4,
|
||||
"kind": "transformers", "hf_id": "facebook/musicgen-small",
|
||||
"deps": ["torch", "torchaudio", "transformers", "scipy", "soundfile"],
|
||||
"label": "MusicGen Small (300MB · 모든 기기)",
|
||||
},
|
||||
"musicgen-medium": {
|
||||
"disk_gb": 1.5, "ram_gb": 6,
|
||||
"kind": "transformers", "hf_id": "facebook/musicgen-medium",
|
||||
"deps": ["torch", "torchaudio", "transformers", "scipy", "soundfile"],
|
||||
"label": "MusicGen Medium (1.5GB · 8GB+ RAM)",
|
||||
},
|
||||
"musicgen-large": {
|
||||
"disk_gb": 3.3, "ram_gb": 12,
|
||||
"kind": "transformers", "hf_id": "facebook/musicgen-large",
|
||||
"deps": ["torch", "torchaudio", "transformers", "scipy", "soundfile"],
|
||||
"label": "MusicGen Large (3.3GB · 16GB+ RAM)",
|
||||
},
|
||||
"acestep-base": {
|
||||
"disk_gb": 10, "ram_gb": 16,
|
||||
"kind": "acestep", "hf_id": "ACE-Step/Ace-Step1.5",
|
||||
"repo": "https://github.com/ace-step/ACE-Step-1.5.git",
|
||||
"label": "ACE-Step 1.5 Base (10GB · 16GB+ Mac/CUDA)",
|
||||
},
|
||||
"acestep-xl": {
|
||||
"disk_gb": 15, "ram_gb": 24,
|
||||
"kind": "acestep", "hf_id": "ACE-Step/acestep-v15-xl-base",
|
||||
"repo": "https://github.com/ace-step/ACE-Step-1.5.git",
|
||||
"label": "ACE-Step 1.5 XL (15GB · 32GB+ 머신)",
|
||||
},
|
||||
}
|
||||
|
||||
DEFAULT_INSTALL_DIR = os.path.expanduser("~/connect-ai-music")
|
||||
|
||||
|
||||
def _log(msg, kind="info"):
|
||||
prefix = {"info": "🔧", "ok": "✅", "warn": "⚠️ ", "err": "❌"}.get(kind, "•")
|
||||
print(f"{prefix} {msg}", file=sys.stderr, flush=True)
|
||||
|
||||
|
||||
def _which(cmd):
|
||||
return shutil.which(cmd) is not None
|
||||
|
||||
|
||||
def _system_ram_gb():
|
||||
"""Detect system RAM. Cross-platform best effort."""
|
||||
try:
|
||||
import psutil
|
||||
return psutil.virtual_memory().total / (1024 ** 3)
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
if sys.platform == "darwin":
|
||||
r = subprocess.run(["sysctl", "-n", "hw.memsize"], capture_output=True, text=True)
|
||||
return int(r.stdout.strip()) / (1024 ** 3)
|
||||
if sys.platform == "linux":
|
||||
with open("/proc/meminfo") as f:
|
||||
for line in f:
|
||||
if line.startswith("MemTotal:"):
|
||||
return int(line.split()[1]) / (1024 ** 2)
|
||||
except Exception:
|
||||
pass
|
||||
return 16 # 보수적 default
|
||||
|
||||
|
||||
def _recommend_model(ram_gb):
|
||||
"""RAM 기반 추천 모델. v2.89.78 — 보수적으로 small 우선.
|
||||
추론할 때 모델 weight + activation + scratch buffer 합쳐서 명시 RAM의 1.5~2배
|
||||
실제 압박 발생. medium은 6GB 명시지만 실제로 12GB+ 압박. 16GB Mac에서 OS·브라우저·
|
||||
VS Code 띄운 상태면 medium 추론 중 swap 폭발. small이 모든 환경에서 안정적이고
|
||||
품질도 충분. 사용자가 원하면 MODEL 필드로 직접 medium/large 선택."""
|
||||
return "musicgen-small"
|
||||
|
||||
|
||||
def _run(cmd, cwd=None):
|
||||
_log(f"$ {' '.join(cmd) if isinstance(cmd, list) else cmd}")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd if isinstance(cmd, list) else cmd.split(),
|
||||
cwd=cwd, check=False, capture_output=True, text=True
|
||||
)
|
||||
# stderr가 진짜 에러면 표시, 아니면 진행상황으로 간주 (pip 등은 진행상황을 stderr에)
|
||||
for stream in (result.stdout, result.stderr):
|
||||
if stream and stream.strip():
|
||||
for line in stream.splitlines()[-20:]: # 마지막 20줄만
|
||||
_log(f" {line}")
|
||||
return result.returncode == 0
|
||||
except Exception as e:
|
||||
_log(f"실행 오류: {e}", "err")
|
||||
return False
|
||||
|
||||
|
||||
def _load_config():
|
||||
if os.path.exists(CONFIG_PATH):
|
||||
try:
|
||||
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
pass
|
||||
return {}
|
||||
|
||||
|
||||
def _save_config(cfg):
|
||||
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
|
||||
json.dump(cfg, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def _install_transformers_model(model_key, install_dir):
|
||||
"""MusicGen 류 — pip + huggingface 다운로드. 가벼운 경로."""
|
||||
info = MODELS[model_key]
|
||||
venv = os.path.join(install_dir, ".venv")
|
||||
|
||||
# venv 생성
|
||||
if not os.path.isdir(venv):
|
||||
_log("Python venv 생성...")
|
||||
if not _run(["python3", "-m", "venv", venv]):
|
||||
return False, "venv 생성 실패"
|
||||
|
||||
venv_pip = os.path.join(venv, "bin", "pip")
|
||||
venv_python = os.path.join(venv, "bin", "python")
|
||||
if not os.path.exists(venv_pip):
|
||||
venv_pip = os.path.join(venv, "Scripts", "pip.exe")
|
||||
venv_python = os.path.join(venv, "Scripts", "python.exe")
|
||||
|
||||
_log("Python 의존성 설치 (1~3분, ~500MB)...")
|
||||
_run([venv_pip, "install", "--upgrade", "pip", "--quiet"])
|
||||
if not _run([venv_pip, "install", "--quiet"] + info["deps"]):
|
||||
return False, "pip install 실패"
|
||||
|
||||
# 모델 weight 다운로드
|
||||
# v2.89.74 — transformers/HF Hub의 verbose 로그 억제. 이전엔 사용자한테
|
||||
# "decoder.model.decoder.embed_positions.weights | UNEXPECTED" 같은 내부 로그 노출돼 혼란.
|
||||
_log(f"모델 다운로드 중: {info['hf_id']} ({info['disk_gb']}GB)...")
|
||||
download_script = f"""
|
||||
import os
|
||||
os.environ['TRANSFORMERS_VERBOSITY'] = 'error'
|
||||
os.environ['TRANSFORMERS_NO_ADVISORY_WARNINGS'] = '1'
|
||||
import logging, warnings
|
||||
logging.getLogger('transformers').setLevel(logging.ERROR)
|
||||
logging.getLogger('huggingface_hub').setLevel(logging.WARNING)
|
||||
warnings.filterwarnings('ignore')
|
||||
print('🔧 라이브러리 로드 중...', flush=True)
|
||||
from transformers import MusicgenForConditionalGeneration, AutoProcessor
|
||||
print('🔧 토크나이저/프로세서 다운로드 중...', flush=True)
|
||||
AutoProcessor.from_pretrained('{info['hf_id']}')
|
||||
print('🔧 모델 weight 다운로드 중 (대용량, 시간 걸림)...', flush=True)
|
||||
MusicgenForConditionalGeneration.from_pretrained('{info['hf_id']}')
|
||||
print('✅ 모델 다운로드·로드 검증 완료')
|
||||
"""
|
||||
if not _run([venv_python, "-c", download_script]):
|
||||
return False, "모델 다운로드 실패 — 인터넷 연결 확인"
|
||||
|
||||
return True, venv_python
|
||||
|
||||
|
||||
def _install_acestep(model_key, install_dir):
|
||||
"""ACE-Step — git clone + 큰 의존성. 무거운 경로."""
|
||||
info = MODELS[model_key]
|
||||
repo_dir = os.path.join(install_dir, "ace-step")
|
||||
|
||||
if not os.path.isdir(repo_dir):
|
||||
_log(f"ACE-Step 1.5 clone 중 → {repo_dir}")
|
||||
if not _run(["git", "clone", "--depth", "1", info["repo"], repo_dir]):
|
||||
return False, "git clone 실패"
|
||||
|
||||
venv = os.path.join(repo_dir, ".venv")
|
||||
if not os.path.isdir(venv):
|
||||
_log("Python venv 생성...")
|
||||
if not _run(["python3", "-m", "venv", venv]):
|
||||
return False, "venv 생성 실패"
|
||||
|
||||
venv_pip = os.path.join(venv, "bin", "pip")
|
||||
venv_python = os.path.join(venv, "bin", "python")
|
||||
if not os.path.exists(venv_pip):
|
||||
venv_pip = os.path.join(venv, "Scripts", "pip.exe")
|
||||
venv_python = os.path.join(venv, "Scripts", "python.exe")
|
||||
|
||||
requirements = os.path.join(repo_dir, "requirements.txt")
|
||||
if os.path.exists(requirements):
|
||||
_log(f"ACE-Step 의존성 설치 중 (5~10분, 큰 패키지 다운로드)...")
|
||||
_run([venv_pip, "install", "--upgrade", "pip", "--quiet"])
|
||||
if not _run([venv_pip, "install", "-r", requirements]):
|
||||
return False, "pip install 일부 실패 — 다시 실행하면 이어짐"
|
||||
|
||||
_log(f"모델 weight (~{info['disk_gb']}GB) 는 첫 음악 생성 때 자동 다운로드", "info")
|
||||
return True, venv_python
|
||||
|
||||
|
||||
def main():
|
||||
cfg = _load_config()
|
||||
|
||||
# 기본 의존성
|
||||
missing = []
|
||||
if not _which("python3"):
|
||||
missing.append("python3 (https://www.python.org/downloads/)")
|
||||
if not _which("git"):
|
||||
missing.append("git (https://git-scm.com/downloads)")
|
||||
if missing:
|
||||
print("❌ 다음 도구 먼저 설치해주세요:")
|
||||
for m in missing:
|
||||
print(f" - {m}")
|
||||
sys.exit(1)
|
||||
|
||||
# 모델 선택: config의 MODEL 우선, 없으면 RAM 기반 추천
|
||||
requested = (cfg.get("MODEL") or "").strip()
|
||||
ram_gb = _system_ram_gb()
|
||||
if not requested:
|
||||
requested = _recommend_model(ram_gb)
|
||||
_log(f"시스템 RAM {ram_gb:.0f}GB → 안전하게 {requested} 선택 (medium/large는 ⚙️ MODEL 필드에서 직접 지정)", "info")
|
||||
|
||||
if requested not in MODELS:
|
||||
print(f"❌ 알 수 없는 MODEL: {requested}")
|
||||
print(f" 사용 가능: {', '.join(MODELS.keys())}")
|
||||
sys.exit(1)
|
||||
|
||||
info = MODELS[requested]
|
||||
_log(f"설치 모델: {info['label']}")
|
||||
|
||||
# 이미 설치돼있으면 빠르게 종료
|
||||
if cfg.get("INSTALLED_MODEL") == requested and cfg.get("VENV_PYTHON"):
|
||||
venv_python = cfg.get("VENV_PYTHON")
|
||||
if os.path.exists(venv_python):
|
||||
print(f"✅ 이미 설치 완료: {info['label']}")
|
||||
print(f" 📁 {cfg.get('INSTALL_DIR')}")
|
||||
print(f" 🐍 {venv_python}")
|
||||
return
|
||||
|
||||
install_dir = cfg.get("INSTALL_DIR") or DEFAULT_INSTALL_DIR
|
||||
os.makedirs(install_dir, exist_ok=True)
|
||||
|
||||
if info["kind"] == "transformers":
|
||||
ok, result = _install_transformers_model(requested, install_dir)
|
||||
else:
|
||||
ok, result = _install_acestep(requested, install_dir)
|
||||
|
||||
if not ok:
|
||||
print(f"❌ 설치 실패: {result}")
|
||||
sys.exit(1)
|
||||
|
||||
venv_python = result
|
||||
cfg["INSTALLED_MODEL"] = requested
|
||||
cfg["MODEL"] = requested
|
||||
cfg["INSTALL_DIR"] = install_dir
|
||||
cfg["VENV_PYTHON"] = venv_python
|
||||
cfg["INSTALL_KIND"] = info["kind"]
|
||||
cfg["HF_ID"] = info["hf_id"]
|
||||
cfg["INSTALLED_AT"] = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
if info["kind"] == "acestep":
|
||||
cfg["ACE_STEP_DIR"] = os.path.join(install_dir, "ace-step")
|
||||
_save_config(cfg)
|
||||
|
||||
# v2.89.74 — 깔끔한 시각적 완료 카드
|
||||
print()
|
||||
print("━" * 50)
|
||||
print(f"🎉 음악 스튜디오 설치 완료!")
|
||||
print("━" * 50)
|
||||
print()
|
||||
print(f"📦 무엇이 깔렸나:")
|
||||
print(f" • 모델: {info['label']}")
|
||||
print(f" • 위치: {install_dir}")
|
||||
print(f" • 디스크: ~{info['disk_gb']}GB 사용 중")
|
||||
print()
|
||||
print(f"🎼 이제 뭐 할 수 있나:")
|
||||
print(f" • 'music_generate.py' ▶ 클릭 → 30초 BGM 생성")
|
||||
print(f" • 'music_to_video.py' ▶ 클릭 → 영상에 BGM 합성")
|
||||
print()
|
||||
print(f"⚙️ 모델 바꾸고 싶으면 ⚙️ → MODEL 드롭다운에서 선택 → 이 도구 다시 ▶")
|
||||
print()
|
||||
print(f"💡 위 로그에 'WARNING / UNEXPECTED' 보였어도 무시해도 됩니다 —")
|
||||
print(f" transformers 라이브러리 내부 메시지. 설치는 정상 완료.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,31 @@
|
||||
<!-- version: music_v3 -->
|
||||
# 🎬 영상 + BGM 합성
|
||||
|
||||
생성한 BGM을 영상에 자동으로 합쳐서 새 mp4 만들기. ffmpeg 사용.
|
||||
|
||||
## 사용 흐름
|
||||
1. `music_generate.py`로 BGM 먼저 생성 (LAST_OUTPUT 자동 기록됨)
|
||||
2. ⚙️에서 VIDEO_PATH 입력 (영상 파일 절대 경로)
|
||||
3. ▶ 실행
|
||||
4. 같은 폴더에 `<영상이름>_with_bgm.mp4` 생성
|
||||
|
||||
## 시스템 요구
|
||||
- ffmpeg 설치 필수
|
||||
- macOS: `brew install ffmpeg`
|
||||
- Windows: https://ffmpeg.org
|
||||
|
||||
## 설정 (⚙️ 클릭)
|
||||
- `VIDEO_PATH` — 합성할 영상 파일 (mp4, mov 등). 절대 경로
|
||||
- `MUSIC_PATH` — 사용할 BGM 파일. 비워두면 마지막 생성한 BGM 자동 사용
|
||||
- `BGM_VOLUME` — BGM 볼륨 0.0~1.0 (디폴트 0.3 = 30%)
|
||||
- `OUTPUT_PATH` — 결과 영상 경로 (비워두면 원본 옆에 `_with_bgm.mp4`)
|
||||
|
||||
## 동작 원리
|
||||
- 원본 영상의 오디오는 100% 볼륨 유지
|
||||
- BGM은 30%(또는 설정값)로 깔림
|
||||
- BGM이 영상보다 짧으면 자동 loop
|
||||
- 영상보다 길면 자동 cut (영상 길이에 맞춤)
|
||||
- 영상 코덱 그대로 (재인코딩 X = 빠름)
|
||||
|
||||
## 출력
|
||||
mp4 (H.264 영상 + AAC 오디오 믹싱)
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
# version: music_v3
|
||||
"""생성된 BGM을 영상에 합치기 (ffmpeg 래퍼).
|
||||
|
||||
설정에서 VIDEO_PATH 지정 (또는 LAST_GENERATED 자동 사용).
|
||||
영상 길이에 BGM 자동 맞춤 (loop 또는 fade out).
|
||||
"""
|
||||
import os, sys, json, subprocess, shutil
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
GEN_CONFIG = os.path.join(HERE, "music_generate.json")
|
||||
MERGE_CONFIG = os.path.join(HERE, "music_to_video.json")
|
||||
|
||||
|
||||
def _log(msg, kind="info"):
|
||||
prefix = {"info": "🎬", "ok": "✅", "warn": "⚠️ ", "err": "❌"}.get(kind, "•")
|
||||
print(f"{prefix} {msg}", file=sys.stderr, flush=True)
|
||||
|
||||
|
||||
def _load(p):
|
||||
if os.path.exists(p):
|
||||
try:
|
||||
with open(p, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
pass
|
||||
return {}
|
||||
|
||||
|
||||
def main():
|
||||
if not shutil.which("ffmpeg"):
|
||||
print("❌ ffmpeg가 설치돼있지 않아요.")
|
||||
print(" macOS: brew install ffmpeg")
|
||||
print(" Windows: https://ffmpeg.org/download.html")
|
||||
sys.exit(1)
|
||||
|
||||
cfg = _load(MERGE_CONFIG)
|
||||
gen = _load(GEN_CONFIG)
|
||||
|
||||
video_path = (cfg.get("VIDEO_PATH") or "").strip()
|
||||
if not video_path:
|
||||
print("❌ VIDEO_PATH 미설정. ⚙️ 클릭해서 영상 파일 경로 입력해주세요.")
|
||||
sys.exit(1)
|
||||
video_path = os.path.expanduser(video_path)
|
||||
if not os.path.exists(video_path):
|
||||
print(f"❌ 영상 파일 없음: {video_path}")
|
||||
sys.exit(1)
|
||||
|
||||
# BGM 파일: 명시적 또는 마지막 생성된 거 자동
|
||||
music_path = (cfg.get("MUSIC_PATH") or "").strip()
|
||||
if not music_path:
|
||||
music_path = gen.get("LAST_OUTPUT") or ""
|
||||
if not music_path or not os.path.exists(music_path):
|
||||
print("❌ BGM 파일 없음. 먼저 'music_generate.py' 실행해서 BGM 생성하거나,")
|
||||
print(" ⚙️에서 MUSIC_PATH 직접 지정.")
|
||||
sys.exit(1)
|
||||
|
||||
bgm_volume = float(cfg.get("BGM_VOLUME", 0.3)) # 0.0~1.0, 디폴트 30%
|
||||
output_path = cfg.get("OUTPUT_PATH") or video_path.rsplit(".", 1)[0] + "_with_bgm.mp4"
|
||||
|
||||
_log(f"영상: {video_path}")
|
||||
_log(f"BGM: {music_path}")
|
||||
_log(f"BGM 볼륨: {int(bgm_volume * 100)}%")
|
||||
_log(f"출력: {output_path}")
|
||||
|
||||
# ffmpeg: 영상 + BGM 믹싱. 영상 길이에 BGM 맞춤 (BGM이 짧으면 loop, 길면 자름)
|
||||
cmd = [
|
||||
"ffmpeg", "-y",
|
||||
"-i", video_path,
|
||||
"-stream_loop", "-1", # BGM 무한 loop (영상 길이까지)
|
||||
"-i", music_path,
|
||||
"-filter_complex",
|
||||
f"[0:a]volume=1.0[orig];[1:a]volume={bgm_volume}[bgm];[orig][bgm]amix=inputs=2:duration=first[a]",
|
||||
"-map", "0:v",
|
||||
"-map", "[a]",
|
||||
"-c:v", "copy", # 영상 코덱 그대로 (재인코딩 없음 = 빠름)
|
||||
"-c:a", "aac",
|
||||
"-shortest",
|
||||
output_path,
|
||||
]
|
||||
_log("ffmpeg 실행 중...")
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if proc.returncode != 0:
|
||||
print(f"❌ ffmpeg 실패 (exit {proc.returncode})")
|
||||
print(proc.stderr[-1000:])
|
||||
sys.exit(1)
|
||||
|
||||
if not os.path.exists(output_path):
|
||||
print(f"❌ 출력 파일 없음")
|
||||
sys.exit(1)
|
||||
|
||||
size_mb = os.path.getsize(output_path) / (1024 * 1024)
|
||||
print(f"✅ 영상 + BGM 합성 완료")
|
||||
print(f" 📁 {output_path}")
|
||||
print(f" 📊 {size_mb:.1f} MB")
|
||||
print(f" 🎵 BGM 볼륨 {int(bgm_volume * 100)}%로 믹싱됨")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -6,3 +6,19 @@
|
||||
{"role":"assistant","text":"*알겠습니다. 게임 장르를 **퍼즐 게임**으로, 핵심 메커니즘은 **Vibe Coding(언더밸류 신호 활용)** 기반으로 설정하고, 타겟층은 **30대에서 50대** 성인으로 명확히 하여 업무를 진행하겠습니다.*","ts":1778162752649}
|
||||
{"role":"user","text":"지금 에이전트들은 뭘하고 있지?","ts":1778162777309}
|
||||
{"role":"assistant","text":"*에이전트들의 현재 활동 현황을 보고드립니다.* \n\n✅ **완료된 작업:**\n- **Researcher**: 시장 트렌드 및 경쟁 게임 분석 완료.\n- **Writer**: 게임 콘셉트 초안 작성 완료.\n- **Business**: 수익 모델 및 비즈니스 타당성 분석 완료.\n\n🚀 **다음 액션 (Top 3):**\n1. **Writer**: 개발 로드맵 초안 작성 준비.\n2. **Researcher**: 기술적 구현 가능성 검토 리스트 작성 준비.\n3. **Business**: 가격 책정 및 마케팅 전략 초안 작성 준비.","ts":1778162788457}
|
||||
{"role":"user","text":"안녕","ts":1778253105200}
|
||||
{"role":"assistant","text":"*안녕하세요. 무엇을 도와드릴까요?*","ts":1778253118996}
|
||||
{"role":"user","text":"너 이름 뭐야?","ts":1778253370898}
|
||||
{"role":"assistant","text":"*저는 G1nation의 비서입니다. 무엇을 도와드릴까요?*","ts":1778253377155}
|
||||
{"role":"user","text":"아 넌 g1nation의 비서구나","ts":1778253396320}
|
||||
{"role":"assistant","text":"*네, 맞습니다. G1nation의 비서입니다. 필요한 업무가 있으시면 언제든지 말씀해주세요.*","ts":1778253401956}
|
||||
{"role":"user","text":"rxrx 주가 얼마야? 지금 시간 기준","ts":1778253462378}
|
||||
{"role":"assistant","text":"*죄송합니다. 저는 실시간 주가 정보 조회 기능은 제공하지 않습니다. 다른 업무 관련 문의사항이 있으시면 말씀해주세요.*","ts":1778253468902}
|
||||
{"role":"user","text":"그렇구나. 그럼 우리 아이들은 지금 뭐해?","ts":1778253483778}
|
||||
{"role":"assistant","text":"*죄송합니다. 제가 현재 접근할 수 있는 업무 관련 정보나 일정 외에는 해당 내용을 확인할 수 없습니다. 업무 관련 문의사항이 있으시면 말씀해주세요.*","ts":1778253492759}
|
||||
{"role":"user","text":"오늘 회의가 뭐뭐 있었지?","ts":1778253505464}
|
||||
{"role":"user","text":"hi","ts":1778255007069}
|
||||
{"role":"assistant","text":"*안녕하세요. G1nation의 비서입니다. 어떤 업무로 도와드릴까요?*","ts":1778255013787}
|
||||
{"role":"user","text":"안녕","ts":1778255470504}
|
||||
{"role":"assistant","text":"*안녕하세요. G1nation의 비서입니다. 어떤 업무로 도와드릴까요?*","ts":1778255475904}
|
||||
{"role":"user","text":"내 컴퓨터 조작 개능해? 지금 vs code 화면 찍어 보내줘","ts":1778284964996}
|
||||
|
||||
@@ -1,29 +1,62 @@
|
||||
# 🌙 오토 플래너
|
||||
# 🌙 오토 플래너 — 24시간 자율 모드
|
||||
|
||||
트렌드 스나이퍼를 정해진 간격으로 반복 실행해서 패턴 데이터를 쌓아주는 무인 작업자예요. 한 번 트렌드를 보면 지금 잘 되는 영상 한 장만 보이지만, 8시간 동안 2시간마다 4번 보면 "어떤 키워드의 후크가 시간이 지나도 계속 살아남는지"가 보이기 시작합니다 — 자는 동안에 그 작업을 대신해줍니다.
|
||||
트렌드 스나이퍼를 일정 간격으로 무한 반복 실행. 24시간 자율 사이클의 일부로, 자는 동안에도 데이터가 누적됨.
|
||||
|
||||
## 어떻게 도와주나요?
|
||||
- ⏰ N시간마다 `trend_sniper.py`를 자동 실행 (스나이퍼 결과는 매번 sessions/에 누적)
|
||||
- 🛌 잘 때 켜두면 아침에 4~5번분의 트렌드 스냅샷이 쌓여 있어요
|
||||
- 📊 같은 키워드라도 시간대별로 어떤 영상이 새로 떠오르는지 비교 가능
|
||||
- ⏰ N시간마다 `trend_sniper.py`를 자동 실행
|
||||
- 🌙 디폴트는 **무한 반복** — 사용자가 중단할 때까지 매 6시간 실행 (하루 4번)
|
||||
- 📊 매 회차마다 `trend_sniper_report.md`에 누적
|
||||
- 🛌 잘 때 켜두면 아침에 트렌드 스냅샷 4~6개 쌓임
|
||||
|
||||
## 어떤 상황에 켜면 좋나요?
|
||||
- 새 채널 컨셉을 결정하기 전, 며칠치 트렌드를 누적해서 보고 싶을 때
|
||||
- 회사 일/외출 중 백그라운드에서 데이터만 모아두고 싶을 때
|
||||
- 특정 키워드의 알고리즘 반응이 시간대마다 다른지 확인하고 싶을 때
|
||||
## 디폴트 설정 (v2.89.71부터)
|
||||
| 필드 | 디폴트 | 의미 |
|
||||
|---|---|---|
|
||||
| `INTERVAL_HOURS` | **6** | 6시간마다 (하루 4번 = YouTube API quota 안전권) |
|
||||
| `TOTAL_RUN_HOURS` | **0** | **0 = 무한** (사용자가 Ctrl+C 또는 창 닫을 때까지) |
|
||||
|
||||
원래 8시간 디폴트였는데 24시간 자율 모드와 모순돼서 0(무한) 으로 변경.
|
||||
|
||||
## 사용 모드 2가지
|
||||
|
||||
**📌 24시간 자율 모드 (디폴트)**
|
||||
```json
|
||||
{ "INTERVAL_HOURS": 6, "TOTAL_RUN_HOURS": 0 }
|
||||
```
|
||||
사용자가 멈출 때까지 6시간마다 무한 실행. 24시간 자율 사이클(설정의 `connectAiLab.autoCycleEnabled`) 과 호환.
|
||||
|
||||
**📌 제한 모드 (테스트용)**
|
||||
```json
|
||||
{ "INTERVAL_HOURS": 2, "TOTAL_RUN_HOURS": 8 }
|
||||
```
|
||||
8시간만 돌고 종료. 첫 사용·디버깅 시 유용.
|
||||
|
||||
## 시작하기 전 체크
|
||||
- 트렌드 스나이퍼 도구가 먼저 설정돼 있어야 해요 (YouTube API 키, 키워드 목록 등)
|
||||
- 첫 실행 전에 트렌드 스나이퍼를 한 번 수동으로 돌려서 정상 작동 확인을 권장합니다
|
||||
|
||||
## 설정값 (auto_planner.json)
|
||||
- `INTERVAL_HOURS` — 몇 시간마다 실행할지 (기본 2)
|
||||
- `TOTAL_RUN_HOURS` — 총 가동 시간 (기본 8 → 8시간 동안 4회 실행)
|
||||
- 트렌드 스나이퍼 도구가 먼저 설정돼 있어야 해요 (YouTube API 키, TARGET_KEYWORDS)
|
||||
- 첫 실행 시 자동으로 trend_sniper.py 한 번 검증 → 실패하면 본 루프 안 돌고 종료
|
||||
- 검증 통과해야 본 루프 시작
|
||||
|
||||
## 실행 방법
|
||||
패널의 [▶ 실행]을 누르면 시작됩니다. 또는 터미널에서:
|
||||
|
||||
**채팅 패널의 [▶ 실행]** — 24시간 자율 모드면 채팅창이 무한 점유됨. 제한 모드 권장.
|
||||
|
||||
**백그라운드 실행 (24시간 자율 권장)**:
|
||||
```bash
|
||||
python auto_planner.py
|
||||
cd ~/Downloads/지식메모리/_company/_agents/youtube/tools/
|
||||
nohup python3 auto_planner.py > planner.log 2>&1 &
|
||||
```
|
||||
|
||||
⚠️ 이 스크립트는 끝날 때까지 터미널을 점유해요. 백그라운드로 돌리려면 별도 창에서 실행하세요. 중단하려면 Ctrl+C.
|
||||
이러면 VS Code 닫아도 백그라운드에서 계속 돔. 중단하려면:
|
||||
```bash
|
||||
ps aux | grep auto_planner
|
||||
kill <PID>
|
||||
```
|
||||
|
||||
## YouTube API quota 주의
|
||||
- 무료 티어: 일일 10,000 unit
|
||||
- trend_sniper 1회 = 약 600 unit (search × 2 키워드)
|
||||
- 6시간 간격 = 하루 4번 = 2,400 unit (안전)
|
||||
- 1시간 간격은 사용 자제 (24번 = 14,400 unit → 한도 초과)
|
||||
|
||||
## 출력
|
||||
- `trend_sniper_report.md` — 매 회차 분석 보고서 누적
|
||||
- 콘솔: 회차 번호, 가동 시간, 다음 실행 시각
|
||||
|
||||
@@ -17,26 +17,52 @@ def load_config():
|
||||
|
||||
def main():
|
||||
cfg = load_config()
|
||||
interval_h = float(cfg.get("INTERVAL_HOURS", 2))
|
||||
total_h = float(cfg.get("TOTAL_RUN_HOURS", 8))
|
||||
print(f"\n🚀 [오토 플래너] {total_h}시간 동안 {interval_h}시간마다 트렌드 분석 실행")
|
||||
interval_h = float(cfg.get("INTERVAL_HOURS", 6)) # v2.89.71: 디폴트 6시간 (하루 4번)
|
||||
total_h = float(cfg.get("TOTAL_RUN_HOURS", 0)) # v2.89.71: 0 = 무한 (24시간 자율 모드)
|
||||
|
||||
# v2.89.71 — 24시간 자율 모드 본격 지원. TOTAL_RUN_HOURS=0이면 사용자가 멈출 때까지 무한.
|
||||
if total_h <= 0:
|
||||
print(f"\n🌙 [오토 플래너] 24시간 자율 모드 — {interval_h}시간마다 무한 반복")
|
||||
print(f"⚠️ 사용자가 중단(Ctrl+C)할 때까지 계속 실행됩니다.")
|
||||
print(f" 백그라운드로 돌리려면 터미널에서:")
|
||||
print(f" nohup python3 {os.path.abspath(__file__)} > planner.log 2>&1 &")
|
||||
else:
|
||||
print(f"\n🚀 [오토 플래너] {total_h}시간 동안 {interval_h}시간마다 트렌드 분석 (제한 모드)")
|
||||
print(f"⚠️ 종료까지 {total_h}시간 채팅창 점유. Ctrl+C로 중단 가능.")
|
||||
print()
|
||||
|
||||
if not os.path.exists(SNIPER_PATH):
|
||||
print(f"❌ trend_sniper.py를 찾을 수 없어요: {SNIPER_PATH}")
|
||||
sys.exit(1)
|
||||
# 첫 실행 전 trend_sniper.py가 정상 동작하는지 빠르게 검증
|
||||
print("🔍 trend_sniper.py 첫 회차 검증 중 (~30초)...")
|
||||
test_proc = subprocess.run([sys.executable, SNIPER_PATH], capture_output=True, text=True, timeout=300)
|
||||
if test_proc.returncode != 0:
|
||||
print(f"❌ trend_sniper.py 검증 실패 (exit {test_proc.returncode})")
|
||||
print(" 먼저 trend_sniper.py 단독으로 ▶ 실행해서 설정·키워드·LLM 연결 확인 후 재시도.")
|
||||
if test_proc.stderr.strip():
|
||||
print(" 에러 일부:")
|
||||
for line in test_proc.stderr.splitlines()[-5:]:
|
||||
print(f" {line}")
|
||||
sys.exit(1)
|
||||
print("✅ 검증 완료. 본 루프 시작.\n")
|
||||
start = time.time()
|
||||
loop = 0
|
||||
while True:
|
||||
if time.time() - start > total_h * 3600:
|
||||
# v2.89.71 — total_h = 0이면 무한 (24시간 자율 모드)
|
||||
if total_h > 0 and (time.time() - start > total_h * 3600):
|
||||
print("\n☀️ 목표 가동 시간을 채웠어요. 종료합니다.")
|
||||
break
|
||||
loop += 1
|
||||
ts = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
print(f"\n[{ts}] 🤖 {loop}회차 트렌드 스나이핑")
|
||||
elapsed_h = (time.time() - start) / 3600
|
||||
print(f"\n[{ts}] 🤖 {loop}회차 트렌드 스나이핑 (가동 {elapsed_h:.1f}시간)")
|
||||
try:
|
||||
subprocess.run([sys.executable, SNIPER_PATH], check=False)
|
||||
except Exception as e:
|
||||
print(f"❌ 실행 실패: {e}")
|
||||
print(f"⏳ 다음 실행: {interval_h}시간 후")
|
||||
next_at = datetime.datetime.now() + datetime.timedelta(hours=interval_h)
|
||||
print(f"⏳ 다음 실행: {next_at.strftime('%Y-%m-%d %H:%M')} ({interval_h}시간 후)")
|
||||
time.sleep(interval_h * 3600)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# version: telegram_v3
|
||||
"""Competitor Brief — for every channel in COMPETITOR_CHANNELS, pulls their
|
||||
recent top-performing videos and asks the local LLM for a *prescriptive*
|
||||
brief: what should YOU do next, given what's working for them.
|
||||
|
||||
@@ -108,29 +108,62 @@ def main():
|
||||
3. 🎬 파괴적 영상 기획안 — 썸네일 카피, 제목 3개, 후킹 오프닝(첫 5초)
|
||||
"""
|
||||
|
||||
print("🧠 [LLM 분석 중...]")
|
||||
# v2.89.70 — LM Studio (OpenAI 호환 API) + Ollama 둘 다 지원. URL/포트로 자동 감지.
|
||||
is_lm_studio = ('1234' in ollama_url) or ('/v1' in ollama_url)
|
||||
print(f"🧠 [LLM 분석 중... 엔진: {'LM Studio' if is_lm_studio else 'Ollama'}]")
|
||||
|
||||
# 모델 자동 선택 — 엔진별로 다른 endpoint
|
||||
if not model:
|
||||
# Try first available model
|
||||
try:
|
||||
r = requests.get(f"{ollama_url}/api/tags", timeout=5)
|
||||
r.raise_for_status()
|
||||
models = [m["name"] for m in r.json().get("models", [])]
|
||||
if is_lm_studio:
|
||||
# LM Studio: GET /v1/models (OpenAI 호환)
|
||||
base = ollama_url.rstrip('/')
|
||||
if not base.endswith('/v1'):
|
||||
base = base + '/v1'
|
||||
r = requests.get(f"{base}/models", timeout=5)
|
||||
r.raise_for_status()
|
||||
models = [m["id"] for m in r.json().get("data", [])]
|
||||
else:
|
||||
# Ollama: GET /api/tags
|
||||
r = requests.get(f"{ollama_url}/api/tags", timeout=5)
|
||||
r.raise_for_status()
|
||||
models = [m["name"] for m in r.json().get("models", [])]
|
||||
if not models:
|
||||
print("❌ 로컬 LLM에 설치된 모델이 없어요. Ollama/LM Studio에서 모델을 풀(pull)하세요.")
|
||||
print(f"❌ 로컬 LLM에 설치된 모델이 없어요. {'LM Studio' if is_lm_studio else 'Ollama'} 에서 모델 로드/풀하세요.")
|
||||
sys.exit(1)
|
||||
model = models[0]
|
||||
print(f" 자동 선택 모델: {model}")
|
||||
except Exception as e:
|
||||
print(f"❌ 로컬 LLM 연결 실패 ({ollama_url}): {e}")
|
||||
print(f" 엔진 실행 확인: {'LM Studio (포트 1234)' if is_lm_studio else 'Ollama (포트 11434)'}")
|
||||
sys.exit(1)
|
||||
|
||||
# 추론 호출 — 엔진별 다른 endpoint·payload 형식
|
||||
try:
|
||||
r = requests.post(
|
||||
f"{ollama_url}/api/generate",
|
||||
json={"model": model, "prompt": prompt, "stream": False},
|
||||
timeout=180,
|
||||
)
|
||||
r.raise_for_status()
|
||||
report = r.json().get("response", "").strip()
|
||||
if is_lm_studio:
|
||||
base = ollama_url.rstrip('/')
|
||||
if not base.endswith('/v1'):
|
||||
base = base + '/v1'
|
||||
r = requests.post(
|
||||
f"{base}/chat/completions",
|
||||
json={
|
||||
"model": model,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"stream": False,
|
||||
"max_tokens": 2048,
|
||||
},
|
||||
timeout=180,
|
||||
)
|
||||
r.raise_for_status()
|
||||
report = r.json().get("choices", [{}])[0].get("message", {}).get("content", "").strip()
|
||||
else:
|
||||
r = requests.post(
|
||||
f"{ollama_url}/api/generate",
|
||||
json={"model": model, "prompt": prompt, "stream": False},
|
||||
timeout=180,
|
||||
)
|
||||
r.raise_for_status()
|
||||
report = r.json().get("response", "").strip()
|
||||
except Exception as e:
|
||||
print(f"❌ LLM 호출 실패: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
Reference in New Issue
Block a user