[G1-Sync] Manual knowledge update

This commit is contained in:
Antigravity Agent
2026-05-09 21:08:02 +09:00
parent f0befc887a
commit 93ec7e9056
363 changed files with 68333 additions and 64 deletions
@@ -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)