157 lines
5.8 KiB
Python
157 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
"""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.
|
|
|
|
Reads youtube_account.json (api key, competitors, ollama, model) and
|
|
competitor_brief.json (volume)."""
|
|
import os, json, sys, time, datetime
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
ACCOUNT = os.path.join(HERE, "youtube_account.json")
|
|
CONFIG = os.path.join(HERE, "competitor_brief.json")
|
|
REPORT = os.path.join(HERE, "competitor_brief_report.md")
|
|
|
|
def _load(p):
|
|
with open(p, "r", encoding="utf-8") as f:
|
|
return json.load(f)
|
|
|
|
def _resolve_channel_id(youtube, handle):
|
|
h = handle.lstrip("@")
|
|
try:
|
|
r = youtube.search().list(part="snippet", q=h, type="channel", maxResults=1).execute()
|
|
items = r.get("items", [])
|
|
if items:
|
|
return items[0]["snippet"]["channelId"], items[0]["snippet"]["title"]
|
|
except Exception:
|
|
pass
|
|
return None, None
|
|
|
|
def _push_telegram(account, text):
|
|
token = (account.get("TELEGRAM_BOT_TOKEN") or "").strip()
|
|
chat = (account.get("TELEGRAM_CHAT_ID") or "").strip()
|
|
if not token or not chat:
|
|
return
|
|
try:
|
|
import requests
|
|
requests.post(f"https://api.telegram.org/bot{token}/sendMessage",
|
|
json={"chat_id": chat, "text": text[:4000], "parse_mode": "Markdown"},
|
|
timeout=10)
|
|
except Exception:
|
|
pass
|
|
|
|
def main():
|
|
if not os.path.exists(ACCOUNT):
|
|
print("❌ youtube_account.json이 없어요.")
|
|
sys.exit(1)
|
|
acct = _load(ACCOUNT)
|
|
cfg = _load(CONFIG) if os.path.exists(CONFIG) else {}
|
|
api_key = (acct.get("YOUTUBE_API_KEY") or "").strip()
|
|
competitors = acct.get("COMPETITOR_CHANNELS") or []
|
|
if not api_key:
|
|
print("❌ YOUTUBE_API_KEY 비어있음.")
|
|
sys.exit(1)
|
|
if not competitors:
|
|
print("❌ COMPETITOR_CHANNELS가 비어있어요. youtube_account.json에 채워주세요.")
|
|
sys.exit(1)
|
|
top_n = int(cfg.get("TOP_N_PER_CHANNEL", 5))
|
|
lookback = int(cfg.get("LOOKBACK_DAYS", 30))
|
|
ollama_url = (acct.get("OLLAMA_URL") or "http://127.0.0.1:11434").rstrip("/")
|
|
model = acct.get("MODEL") or ""
|
|
|
|
try:
|
|
from googleapiclient.discovery import build
|
|
import requests
|
|
except ImportError:
|
|
print("❌ pip install google-api-python-client requests")
|
|
sys.exit(1)
|
|
youtube = build("youtube", "v3", developerKey=api_key)
|
|
after = (datetime.datetime.utcnow() - datetime.timedelta(days=lookback)).isoformat("T") + "Z"
|
|
|
|
snapshot = []
|
|
for ch in competitors:
|
|
cid, ctitle = _resolve_channel_id(youtube, ch)
|
|
if not cid:
|
|
print(f"⚠️ {ch} 채널 못 찾음")
|
|
continue
|
|
print(f"🔭 [{ch}] 최근 영상 분석 중...")
|
|
sr = youtube.search().list(part="snippet", channelId=cid, maxResults=top_n,
|
|
order="viewCount", publishedAfter=after, type="video").execute()
|
|
ids = [it["id"]["videoId"] for it in sr.get("items", [])]
|
|
if not ids:
|
|
continue
|
|
st = youtube.videos().list(part="statistics,snippet", id=",".join(ids)).execute()
|
|
for it in st.get("items", []):
|
|
stats = it.get("statistics", {})
|
|
snip = it.get("snippet", {})
|
|
snapshot.append({
|
|
"channel": ctitle,
|
|
"title": snip.get("title", ""),
|
|
"views": int(stats.get("viewCount", 0)),
|
|
"published": snip.get("publishedAt", "")[:10],
|
|
})
|
|
|
|
if not snapshot:
|
|
print("❌ 데이터 수집 실패.")
|
|
sys.exit(1)
|
|
|
|
snapshot.sort(key=lambda r: r["views"], reverse=True)
|
|
data_text = "\n".join(f"[{r['channel']}] {r['views']:,}회 · {r['published']} · {r['title']}"
|
|
for r in snapshot[:25])
|
|
|
|
if not 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 not models:
|
|
print("❌ 로컬 LLM에 모델이 없어요.")
|
|
sys.exit(1)
|
|
model = models[0]
|
|
except Exception as e:
|
|
print(f"❌ LLM 연결 실패: {e}")
|
|
sys.exit(1)
|
|
|
|
prompt = f"""당신은 유튜브 알고리즘 전략가입니다. 아래는 경쟁 채널들의 최근 {lookback}일간 상위 영상 데이터입니다.
|
|
|
|
[경쟁 데이터]
|
|
{data_text}
|
|
|
|
이 채널 운영자에게 **지시문 형식**으로 다음을 작성하세요. 모호한 조언 금지, 구체적이고 실행 가능한 지시.
|
|
|
|
## 1) 지금 당장 해야 하는 것 (3개)
|
|
- 각 항목: "~을(를) 하세요. 왜냐하면 …"
|
|
|
|
## 2) 이번 주 안에 시도해야 하는 것 (3개)
|
|
- 각 항목: 구체적 영상 제목 후보 또는 후크 문장 포함
|
|
|
|
## 3) 절대 하지 말아야 할 것 (1개)
|
|
- 경쟁사 데이터에서 보이는 함정 패턴
|
|
|
|
## 4) 한 줄 요약
|
|
- 다음 영상의 핵심 컨셉을 한 문장으로
|
|
"""
|
|
print("🧠 [LLM 분석 중...]")
|
|
try:
|
|
r = requests.post(f"{ollama_url}/api/generate",
|
|
json={"model": model, "prompt": prompt, "stream": False},
|
|
timeout=240)
|
|
r.raise_for_status()
|
|
brief = r.json().get("response", "").strip()
|
|
except Exception as e:
|
|
print(f"❌ LLM 실패: {e}")
|
|
sys.exit(1)
|
|
|
|
ts = time.strftime('%Y-%m-%d %H:%M')
|
|
out = f"# 🔭 경쟁 채널 브리프 — {ts}\n\n채널: {', '.join(competitors)} · 최근 {lookback}일\n\n{brief}\n"
|
|
print("\n" + "="*60)
|
|
print(out)
|
|
print("="*60)
|
|
with open(REPORT, "a", encoding="utf-8") as f:
|
|
f.write("\n\n" + out + "\n---\n")
|
|
print(f"\n✅ 보고서: {REPORT}")
|
|
_push_telegram(acct, out)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|