chore(corporate): session 2026-05-07T13-32

This commit is contained in:
한예성
2026-05-07 22:33:12 +09:00
parent 293c51c703
commit 3069c8aee5
67 changed files with 2693 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
# Connect AI auto-generated
.DS_Store
.obsidian/
.trash/
node_modules/
*.tmp
*.log
.cache/
Thumbs.db
+13
View File
@@ -0,0 +1,13 @@
# 자동 생성 — Connect AI 1인 기업 모드
# 시크릿·API 키 보호
_agents/*/config.md
# 도구 설정 JSON 안에 API 키·텔레그램 봇 토큰이 들어갈 수 있어 git에서 제외
_agents/*/tools/*.json
_agents/*/tools/youtube_account.json
# 외부 API 응답 캐시 (재현 가능)
_cache/
# 대용량 임시 산출물
_tmp/
*.log
+15
View File
@@ -0,0 +1,15 @@
# 📊 Business 에이전트 — 나의 미션
> 🌞 24시간 업무가 켜져 있으면 이 미션을 향해 자동으로 한 스텝씩 일합니다.
> 자유롭게 수정하세요. 비워두면 회사 공동 목표만 따라갑니다.
## 장기 목표 (3~6개월)
- 수익화 모델 1개 가설 검증 → 매출화
- 핵심 KPI 대시보드 운영
## 이번 주 목표
- 가격·번들 옵션 2~3안 비교 메모
- 경쟁사 3곳 ROI 분석
## 작업 원칙
- 결정 가능한 권고 (A/B 중 어느 쪽인지) + 근거 숫자
+5
View File
@@ -0,0 +1,5 @@
# 💰 Business (Head of Business) 개인 메모리
_Business 에이전트만 읽고 쓰는 개인 노트. 학습·교훈·자주 쓰는 패턴이 누적됩니다._
## 학습 기록
+5
View File
@@ -0,0 +1,5 @@
# 💰 Business 페르소나 디테일
_여기에 Business 에이전트에게 주고 싶은 추가 지시·말투·취향·예시 등을 자유롭게 적으세요._
_매 호출 시 시스템 프롬프트에 자동 주입됩니다. (git에 동기화됨)_
+55
View File
@@ -0,0 +1,55 @@
# 💰 Business — 도구 매니페스트
_Business 에이전트가 어떤 도구를 어디까지 자율적으로 쓸 수 있는지 정의합니다._
_매번 시스템 프롬프트로 주입되며, 텔레그램에서 `/tools`로 현재 상태 확인 가능._
---
## 자율도 레벨
AUTONOMY_LEVEL: 2
| 값 | 의미 |
|---|---|
| 0 | Off — 도구 전체 비활성 (이 에이전트는 채팅만) |
| 1 | Read-only — 읽기·분석·보고만, 외부에 쓰기 X |
| 2 | Draft — 초안 작성 후 사용자 승인 게이트 통과해야 실행 ⭐ 권장 기본값 |
| 3 | Auto — 화이트리스트 안에서 사용자 승인 없이 실행 |
> 위 `AUTONOMY_LEVEL` 줄의 숫자(0~3)를 직접 바꾸면 다음 호출부터 적용됩니다.
---
## 사용 가능한 도구
### `revenue_pull`
Stripe/Toss/PayPal 매출 데이터
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `analytics_pull`
Google Analytics / Plausible 트래픽
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `pnl_generator`
월별 P&L 마크다운 자동 생성
- `enabled`: true
- `requires_credentials`: `config.md` 참조
---
## 안전 규칙 (모든 레벨 공통, 절대 우회 X)
- **삭제·배포·발송**(rm, deploy --prod, send, publish) 류는 자율도와 무관하게 **항상 승인 게이트**.
- 외부 API 호출 전 `config.md`의 토큰 존재 여부 확인.
- 모든 외부 행동은 `_agents/business/activity.log`에 한 줄 기록 (감사용).
- 승인 대기 액션은 `approvals/pending/` 에 저장 → 텔레그램 `/approvals` 로 조회.
---
_레벨을 어떻게 골라야 할지 모르겠다면 `2 (Draft)`가 안전한 시작점입니다._
+5
View File
@@ -0,0 +1,5 @@
# 🧭 CEO (Chief Executive Agent) 개인 메모리
_CEO 에이전트만 읽고 쓰는 개인 노트. 학습·교훈·자주 쓰는 패턴이 누적됩니다._
## 학습 기록
+5
View File
@@ -0,0 +1,5 @@
# 🧭 CEO 페르소나 디테일
_여기에 CEO 에이전트에게 주고 싶은 추가 지시·말투·취향·예시 등을 자유롭게 적으세요._
_매 호출 시 시스템 프롬프트에 자동 주입됩니다. (git에 동기화됨)_
+55
View File
@@ -0,0 +1,55 @@
# 🧭 CEO — 도구 매니페스트
_CEO 에이전트가 어떤 도구를 어디까지 자율적으로 쓸 수 있는지 정의합니다._
_매번 시스템 프롬프트로 주입되며, 텔레그램에서 `/tools`로 현재 상태 확인 가능._
---
## 자율도 레벨
AUTONOMY_LEVEL: 2
| 값 | 의미 |
|---|---|
| 0 | Off — 도구 전체 비활성 (이 에이전트는 채팅만) |
| 1 | Read-only — 읽기·분석·보고만, 외부에 쓰기 X |
| 2 | Draft — 초안 작성 후 사용자 승인 게이트 통과해야 실행 ⭐ 권장 기본값 |
| 3 | Auto — 화이트리스트 안에서 사용자 승인 없이 실행 |
> 위 `AUTONOMY_LEVEL` 줄의 숫자(0~3)를 직접 바꾸면 다음 호출부터 적용됩니다.
---
## 사용 가능한 도구
### `approval_gate`
위험 액션(deploy/post/send/rm) 사용자 승인 게이트
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `team_briefing`
주간 전체 회의 자동 진행 + 회의록 정리
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `router`
사용자 명령 → 적합한 specialist로 분배
- `enabled`: true
- `requires_credentials`: `config.md` 참조
---
## 안전 규칙 (모든 레벨 공통, 절대 우회 X)
- **삭제·배포·발송**(rm, deploy --prod, send, publish) 류는 자율도와 무관하게 **항상 승인 게이트**.
- 외부 API 호출 전 `config.md`의 토큰 존재 여부 확인.
- 모든 외부 행동은 `_agents/ceo/activity.log`에 한 줄 기록 (감사용).
- 승인 대기 액션은 `approvals/pending/` 에 저장 → 텔레그램 `/approvals` 로 조회.
---
_레벨을 어떻게 골라야 할지 모르겠다면 `2 (Draft)`가 안전한 시작점입니다._
+15
View File
@@ -0,0 +1,15 @@
# 🎨 Designer 에이전트 — 나의 미션
> 🌞 24시간 업무가 켜져 있으면 이 미션을 향해 자동으로 한 스텝씩 일합니다.
> 자유롭게 수정하세요. 비워두면 회사 공동 목표만 따라갑니다.
## 장기 목표 (3~6개월)
- 브랜드 컬러·타이포·로고 시스템 확정
- 썸네일/포스트 템플릿 3종 표준화
## 이번 주 목표
- 디자인 브리프 1건 작성 (레퍼런스 5장 포함)
- 썸네일 컨셉 3안 비교 정리
## 작업 원칙
- 텍스트 설명만 X — 색상 코드·폰트명·레이아웃 좌표까지 구체적으로
+5
View File
@@ -0,0 +1,5 @@
# 🎨 Designer (Lead Designer) 개인 메모리
_Designer 에이전트만 읽고 쓰는 개인 노트. 학습·교훈·자주 쓰는 패턴이 누적됩니다._
## 학습 기록
+5
View File
@@ -0,0 +1,5 @@
# 🎨 Designer 페르소나 디테일
_여기에 Designer 에이전트에게 주고 싶은 추가 지시·말투·취향·예시 등을 자유롭게 적으세요._
_매 호출 시 시스템 프롬프트에 자동 주입됩니다. (git에 동기화됨)_
+61
View File
@@ -0,0 +1,61 @@
# 🎨 Designer — 도구 매니페스트
_Designer 에이전트가 어떤 도구를 어디까지 자율적으로 쓸 수 있는지 정의합니다._
_매번 시스템 프롬프트로 주입되며, 텔레그램에서 `/tools`로 현재 상태 확인 가능._
---
## 자율도 레벨
AUTONOMY_LEVEL: 2
| 값 | 의미 |
|---|---|
| 0 | Off — 도구 전체 비활성 (이 에이전트는 채팅만) |
| 1 | Read-only — 읽기·분석·보고만, 외부에 쓰기 X |
| 2 | Draft — 초안 작성 후 사용자 승인 게이트 통과해야 실행 ⭐ 권장 기본값 |
| 3 | Auto — 화이트리스트 안에서 사용자 승인 없이 실행 |
> 위 `AUTONOMY_LEVEL` 줄의 숫자(0~3)를 직접 바꾸면 다음 호출부터 적용됩니다.
---
## 사용 가능한 도구
### `image_local`
로컬 SDXL/FLUX 이미지 생성 (오프라인 정체성)
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `image_cloud`
DALL-E/Replicate (Connected 모드 토글)
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `brand_check`
브랜드 색상 팔레트·타이포 일관성 검증
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `asset_library`
_company/assets/ 자동 정리·태깅
- `enabled`: true
- `requires_credentials`: `config.md` 참조
---
## 안전 규칙 (모든 레벨 공통, 절대 우회 X)
- **삭제·배포·발송**(rm, deploy --prod, send, publish) 류는 자율도와 무관하게 **항상 승인 게이트**.
- 외부 API 호출 전 `config.md`의 토큰 존재 여부 확인.
- 모든 외부 행동은 `_agents/designer/activity.log`에 한 줄 기록 (감사용).
- 승인 대기 액션은 `approvals/pending/` 에 저장 → 텔레그램 `/approvals` 로 조회.
---
_레벨을 어떻게 골라야 할지 모르겠다면 `2 (Draft)`가 안전한 시작점입니다._
+16
View File
@@ -0,0 +1,16 @@
# 💻 Developer 에이전트 — 나의 미션
> 🌞 24시간 업무가 켜져 있으면 이 미션을 향해 자동으로 한 스텝씩 일합니다.
> 자유롭게 수정하세요. 비워두면 회사 공동 목표만 따라갑니다.
## 장기 목표 (3~6개월)
- 반복 업무 자동화 스크립트 5개 운영
- 데이터 파이프라인 / API 연결 안정화
## 이번 주 목표
- 가장 시간 잡아먹는 수동 작업 1개 자동화
- 기존 스크립트 1개 리팩터·테스트 보강
## 작업 원칙
- 항상 실행 가능한 코드 + 사용법 1줄
- 외부 호출은 키 노출 없이 환경변수로
+5
View File
@@ -0,0 +1,5 @@
# 💻 Developer (Lead Engineer) 개인 메모리
_Developer 에이전트만 읽고 쓰는 개인 노트. 학습·교훈·자주 쓰는 패턴이 누적됩니다._
## 학습 기록
+5
View File
@@ -0,0 +1,5 @@
# 💻 Developer 페르소나 디테일
_여기에 Developer 에이전트에게 주고 싶은 추가 지시·말투·취향·예시 등을 자유롭게 적으세요._
_매 호출 시 시스템 프롬프트에 자동 주입됩니다. (git에 동기화됨)_
+67
View File
@@ -0,0 +1,67 @@
# 💻 Developer — 도구 매니페스트
_Developer 에이전트가 어떤 도구를 어디까지 자율적으로 쓸 수 있는지 정의합니다._
_매번 시스템 프롬프트로 주입되며, 텔레그램에서 `/tools`로 현재 상태 확인 가능._
---
## 자율도 레벨
AUTONOMY_LEVEL: 2
| 값 | 의미 |
|---|---|
| 0 | Off — 도구 전체 비활성 (이 에이전트는 채팅만) |
| 1 | Read-only — 읽기·분석·보고만, 외부에 쓰기 X |
| 2 | Draft — 초안 작성 후 사용자 승인 게이트 통과해야 실행 ⭐ 권장 기본값 |
| 3 | Auto — 화이트리스트 안에서 사용자 승인 없이 실행 |
> 위 `AUTONOMY_LEVEL` 줄의 숫자(0~3)를 직접 바꾸면 다음 호출부터 적용됩니다.
---
## 사용 가능한 도구
### `project_scaffolder`
_company/projects/<name>/ 폴더 자동 생성 (vite/next/astro)
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `dev_server`
자체 dev server + 포트 매니저 + 라이브 미리보기 푸시
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `git_committer`
작업 단위 자동 커밋
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `deploy_cli`
Vercel/Netlify/Cloudflare 배포 (deploy --prod는 항상 승인)
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `lint_test`
테스트·린트·타입체크 자동 실행
- `enabled`: true
- `requires_credentials`: `config.md` 참조
---
## 안전 규칙 (모든 레벨 공통, 절대 우회 X)
- **삭제·배포·발송**(rm, deploy --prod, send, publish) 류는 자율도와 무관하게 **항상 승인 게이트**.
- 외부 API 호출 전 `config.md`의 토큰 존재 여부 확인.
- 모든 외부 행동은 `_agents/developer/activity.log`에 한 줄 기록 (감사용).
- 승인 대기 액션은 `approvals/pending/` 에 저장 → 텔레그램 `/approvals` 로 조회.
---
_레벨을 어떻게 골라야 할지 모르겠다면 `2 (Draft)`가 안전한 시작점입니다._
+15
View File
@@ -0,0 +1,15 @@
# ✂️ Editor 에이전트 — 나의 미션
> 🌞 24시간 업무가 켜져 있으면 이 미션을 향해 자동으로 한 스텝씩 일합니다.
> 자유롭게 수정하세요. 비워두면 회사 공동 목표만 따라갑니다.
## 장기 목표 (3~6개월)
- 영상 편집 디렉션 템플릿 (오프닝·B-roll·아웃트로) 표준화
- 평균 컷 리듬·자막 톤 가이드 확립
## 이번 주 목표
- 최근 영상 1편 컷·자막·B-roll 디렉션 작성
- 스크립트 1편 다듬기 (불필요 문장 제거)
## 작업 원칙
- 막연한 "다듬어줘" X — 시간 코드 + 구체 액션
+5
View File
@@ -0,0 +1,5 @@
# ✂️ Editor (Video & Content Editor) 개인 메모리
_Editor 에이전트만 읽고 쓰는 개인 노트. 학습·교훈·자주 쓰는 패턴이 누적됩니다._
## 학습 기록
+5
View File
@@ -0,0 +1,5 @@
# ✂️ Editor 페르소나 디테일
_여기에 Editor 에이전트에게 주고 싶은 추가 지시·말투·취향·예시 등을 자유롭게 적으세요._
_매 호출 시 시스템 프롬프트에 자동 주입됩니다. (git에 동기화됨)_
+55
View File
@@ -0,0 +1,55 @@
# ✂️ Editor — 도구 매니페스트
_Editor 에이전트가 어떤 도구를 어디까지 자율적으로 쓸 수 있는지 정의합니다._
_매번 시스템 프롬프트로 주입되며, 텔레그램에서 `/tools`로 현재 상태 확인 가능._
---
## 자율도 레벨
AUTONOMY_LEVEL: 2
| 값 | 의미 |
|---|---|
| 0 | Off — 도구 전체 비활성 (이 에이전트는 채팅만) |
| 1 | Read-only — 읽기·분석·보고만, 외부에 쓰기 X |
| 2 | Draft — 초안 작성 후 사용자 승인 게이트 통과해야 실행 ⭐ 권장 기본값 |
| 3 | Auto — 화이트리스트 안에서 사용자 승인 없이 실행 |
> 위 `AUTONOMY_LEVEL` 줄의 숫자(0~3)를 직접 바꾸면 다음 호출부터 적용됩니다.
---
## 사용 가능한 도구
### `ffmpeg_runner`
컷·자막·B-roll 합성
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `whisper_local`
로컬 자막 생성 (오프라인)
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `reframe_9_16`
16:9 → 9:16 자동 리프레임 (릴스/숏츠)
- `enabled`: true
- `requires_credentials`: `config.md` 참조
---
## 안전 규칙 (모든 레벨 공통, 절대 우회 X)
- **삭제·배포·발송**(rm, deploy --prod, send, publish) 류는 자율도와 무관하게 **항상 승인 게이트**.
- 외부 API 호출 전 `config.md`의 토큰 존재 여부 확인.
- 모든 외부 행동은 `_agents/editor/activity.log`에 한 줄 기록 (감사용).
- 승인 대기 액션은 `approvals/pending/` 에 저장 → 텔레그램 `/approvals` 로 조회.
---
_레벨을 어떻게 골라야 할지 모르겠다면 `2 (Draft)`가 안전한 시작점입니다._
+15
View File
@@ -0,0 +1,15 @@
# 📸 Instagram 에이전트 — 나의 미션
> 🌞 24시간 업무가 켜져 있으면 이 미션을 향해 자동으로 한 스텝씩 일합니다.
> 자유롭게 수정하세요. 비워두면 회사 공동 목표만 따라갑니다.
## 장기 목표 (3~6개월)
- 피드 톤앤매너 확립 + 팔로워 5천 도달
- 릴스 평균 도달 1만 이상
## 이번 주 목표
- 릴스 기획 3개 (훅·보이스오버·자막 포함)
- 캡션·해시태그 패턴 정리
## 작업 원칙
- 매 산출물마다 게시 시간 + 후속 스토리 아이디어 1개
+5
View File
@@ -0,0 +1,5 @@
# 📷 Instagram (Head of Instagram) 개인 메모리
_Instagram 에이전트만 읽고 쓰는 개인 노트. 학습·교훈·자주 쓰는 패턴이 누적됩니다._
## 학습 기록
+5
View File
@@ -0,0 +1,5 @@
# 📷 Instagram 페르소나 디테일
_여기에 Instagram 에이전트에게 주고 싶은 추가 지시·말투·취향·예시 등을 자유롭게 적으세요._
_매 호출 시 시스템 프롬프트에 자동 주입됩니다. (git에 동기화됨)_
+61
View File
@@ -0,0 +1,61 @@
# 📷 Instagram — 도구 매니페스트
_Instagram 에이전트가 어떤 도구를 어디까지 자율적으로 쓸 수 있는지 정의합니다._
_매번 시스템 프롬프트로 주입되며, 텔레그램에서 `/tools`로 현재 상태 확인 가능._
---
## 자율도 레벨
AUTONOMY_LEVEL: 2
| 값 | 의미 |
|---|---|
| 0 | Off — 도구 전체 비활성 (이 에이전트는 채팅만) |
| 1 | Read-only — 읽기·분석·보고만, 외부에 쓰기 X |
| 2 | Draft — 초안 작성 후 사용자 승인 게이트 통과해야 실행 ⭐ 권장 기본값 |
| 3 | Auto — 화이트리스트 안에서 사용자 승인 없이 실행 |
> 위 `AUTONOMY_LEVEL` 줄의 숫자(0~3)를 직접 바꾸면 다음 호출부터 적용됩니다.
---
## 사용 가능한 도구
### `instagram_account`
Meta Graph API OAuth (비즈니스 계정)
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `feed_poster`
피드/스토리/릴스 게시 (Draft → 승인 → 게시)
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `dm_responder`
DM·댓글 분류 + 답글 초안
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `insights_pull`
도달·참여·팔로워 추이
- `enabled`: true
- `requires_credentials`: `config.md` 참조
---
## 안전 규칙 (모든 레벨 공통, 절대 우회 X)
- **삭제·배포·발송**(rm, deploy --prod, send, publish) 류는 자율도와 무관하게 **항상 승인 게이트**.
- 외부 API 호출 전 `config.md`의 토큰 존재 여부 확인.
- 모든 외부 행동은 `_agents/instagram/activity.log`에 한 줄 기록 (감사용).
- 승인 대기 액션은 `approvals/pending/` 에 저장 → 텔레그램 `/approvals` 로 조회.
---
_레벨을 어떻게 골라야 할지 모르겠다면 `2 (Draft)`가 안전한 시작점입니다._
+15
View File
@@ -0,0 +1,15 @@
# 🔍 Researcher 에이전트 — 나의 미션
> 🌞 24시간 업무가 켜져 있으면 이 미션을 향해 자동으로 한 스텝씩 일합니다.
> 자유롭게 수정하세요. 비워두면 회사 공동 목표만 따라갑니다.
## 장기 목표 (3~6개월)
- 산업·경쟁사 트렌드 리포트 월 1회 발행
- 인용 가능한 1차 자료 라이브러리 구축
## 이번 주 목표
- 우리 분야 트렌드 5개 짧은 메모
- 경쟁사 2곳 최근 활동·성공 콘텐츠 정리
## 작업 원칙
- 출처 링크 필수, 의견과 사실 분리해서 표기
+5
View File
@@ -0,0 +1,5 @@
# 🔍 Researcher (Trend & Data Researcher) 개인 메모리
_Researcher 에이전트만 읽고 쓰는 개인 노트. 학습·교훈·자주 쓰는 패턴이 누적됩니다._
## 학습 기록
+5
View File
@@ -0,0 +1,5 @@
# 🔍 Researcher 페르소나 디테일
_여기에 Researcher 에이전트에게 주고 싶은 추가 지시·말투·취향·예시 등을 자유롭게 적으세요._
_매 호출 시 시스템 프롬프트에 자동 주입됩니다. (git에 동기화됨)_
+55
View File
@@ -0,0 +1,55 @@
# 🔍 Researcher — 도구 매니페스트
_Researcher 에이전트가 어떤 도구를 어디까지 자율적으로 쓸 수 있는지 정의합니다._
_매번 시스템 프롬프트로 주입되며, 텔레그램에서 `/tools`로 현재 상태 확인 가능._
---
## 자율도 레벨
AUTONOMY_LEVEL: 2
| 값 | 의미 |
|---|---|
| 0 | Off — 도구 전체 비활성 (이 에이전트는 채팅만) |
| 1 | Read-only — 읽기·분석·보고만, 외부에 쓰기 X |
| 2 | Draft — 초안 작성 후 사용자 승인 게이트 통과해야 실행 ⭐ 권장 기본값 |
| 3 | Auto — 화이트리스트 안에서 사용자 승인 없이 실행 |
> 위 `AUTONOMY_LEVEL` 줄의 숫자(0~3)를 직접 바꾸면 다음 호출부터 적용됩니다.
---
## 사용 가능한 도구
### `web_search`
Brave/DuckDuckGo 검색 (Connected)
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `page_fetcher`
본문 추출 + 출처 인용
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `monitor_daily`
매일 내 분야 뉴스 → CEO 브리핑
- `enabled`: true
- `requires_credentials`: `config.md` 참조
---
## 안전 규칙 (모든 레벨 공통, 절대 우회 X)
- **삭제·배포·발송**(rm, deploy --prod, send, publish) 류는 자율도와 무관하게 **항상 승인 게이트**.
- 외부 API 호출 전 `config.md`의 토큰 존재 여부 확인.
- 모든 외부 행동은 `_agents/researcher/activity.log`에 한 줄 기록 (감사용).
- 승인 대기 액션은 `approvals/pending/` 에 저장 → 텔레그램 `/approvals` 로 조회.
---
_레벨을 어떻게 골라야 할지 모르겠다면 `2 (Draft)`가 안전한 시작점입니다._
+15
View File
@@ -0,0 +1,15 @@
# 🗂️ Secretary 에이전트 — 나의 미션
> 🌞 24시간 업무가 켜져 있으면 이 미션을 향해 자동으로 한 스텝씩 일합니다.
> 자유롭게 수정하세요. 비워두면 회사 공동 목표만 따라갑니다.
## 장기 목표 (3~6개월)
- 데일리 브리핑·할 일 정리 루틴 자동화
- 다른 에이전트 산출물을 한 줄 요약으로 모아서 보고
## 이번 주 목표
- 매일 09:00 데일리 브리핑 정리
- 미해결 할 일 5건 추적 + 다음 액션 명시
## 작업 원칙
- "정리"보다 "다음 액션 1개" 명시가 우선
+5
View File
@@ -0,0 +1,5 @@
# 📱 Secretary (Personal Assistant) 개인 메모리
_Secretary 에이전트만 읽고 쓰는 개인 노트. 학습·교훈·자주 쓰는 패턴이 누적됩니다._
## 학습 기록
+5
View File
@@ -0,0 +1,5 @@
# 📱 Secretary 페르소나 디테일
_여기에 Secretary 에이전트에게 주고 싶은 추가 지시·말투·취향·예시 등을 자유롭게 적으세요._
_매 호출 시 시스템 프롬프트에 자동 주입됩니다. (git에 동기화됨)_
+67
View File
@@ -0,0 +1,67 @@
# 📱 영숙 — 도구 매니페스트
_영숙 에이전트가 어떤 도구를 어디까지 자율적으로 쓸 수 있는지 정의합니다._
_매번 시스템 프롬프트로 주입되며, 텔레그램에서 `/tools`로 현재 상태 확인 가능._
---
## 자율도 레벨
AUTONOMY_LEVEL: 2
| 값 | 의미 |
|---|---|
| 0 | Off — 도구 전체 비활성 (이 에이전트는 채팅만) |
| 1 | Read-only — 읽기·분석·보고만, 외부에 쓰기 X |
| 2 | Draft — 초안 작성 후 사용자 승인 게이트 통과해야 실행 ⭐ 권장 기본값 |
| 3 | Auto — 화이트리스트 안에서 사용자 승인 없이 실행 |
> 위 `AUTONOMY_LEVEL` 줄의 숫자(0~3)를 직접 바꾸면 다음 호출부터 적용됩니다.
---
## 사용 가능한 도구
### `calendar_local`
_agents/secretary/calendar.md (Lv.1 오프라인)
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `calendar_caldav`
CalDAV (iCloud/Google 호환, Connected 토글)
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `telegram_bot`
텔레그램 양방향 봇 (이미 활성)
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `kakao_alert`
카카오톡 "나에게 보내기" 단방향 알림
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `email_triage`
IMAP/Gmail 분류 + 답장 초안
- `enabled`: true
- `requires_credentials`: `config.md` 참조
---
## 안전 규칙 (모든 레벨 공통, 절대 우회 X)
- **삭제·배포·발송**(rm, deploy --prod, send, publish) 류는 자율도와 무관하게 **항상 승인 게이트**.
- 외부 API 호출 전 `config.md`의 토큰 존재 여부 확인.
- 모든 외부 행동은 `_agents/secretary/activity.log`에 한 줄 기록 (감사용).
- 승인 대기 액션은 `approvals/pending/` 에 저장 → 텔레그램 `/approvals` 로 조회.
---
_레벨을 어떻게 골라야 할지 모르겠다면 `2 (Draft)`가 안전한 시작점입니다._
@@ -0,0 +1,34 @@
# 📅 Google Calendar
비서가 본인의 Google Calendar와 양방향 연결됩니다 — 다가오는 일정 자동 동기화 + 마감일(due) 있는 추적 작업을 자동으로 캘린더에 등록 (5분 전·1시간 전 알림 자동).
## 무엇을 추가로 하나요? (vs iCal 읽기 전용)
- ✍️ **자동 일정 생성** — 추적기에 due 들어가면 즉시 캘린더에 일정 만듦
- 🔁 일정 수정·삭제도 가능 (작업 완료/취소 시 캘린더 정리)
- 🔔 알림 자동 셋팅 (5분 전, 1시간 전 팝업)
- 📥 동시에 읽기도 가능 (별도 iCal 셋업 불필요)
## 셋업 (한 번만, 5~10분)
명령 팔레트 → **`Connect AI: Google Calendar 자동 일정 연결 📅`** 실행하면 마법사가 안내합니다:
1. Google Cloud Console에서 OAuth 클라이언트 만들기 (가이드 따라 클릭)
2. Client ID + Secret 붙여넣기
3. 브라우저로 로그인 → 끝
## 동작 방식
- 사용자: *"내일까지 광고주 자료 정리해야 해"* 라고 텔레그램으로 시킴
- 비서: 추적기 등록 + 자동으로 `내일 09:00` Google Calendar에 일정 생성
- 알림: 5분 전, 1시간 전 자동 팝업
## 설정 (⚙️에서 조정 가능)
- `CALENDAR_ID` — 기본 `primary` (본인 메인 캘린더). 다른 캘린더 ID 가능
- `DEFAULT_DURATION_MINUTES` — 기본 60분. 작업 일정 길이가 명시 안 됐을 때 사용
## ▶ 실행하면?
현재 연결 상태와 설정값을 진단 출력합니다 (이벤트 생성 X). 진짜 일정 등록은 추적 작업이 들어올 때 자동.
## 보안
- Client ID/Secret/Refresh Token은 `google_calendar_write.json` 한 파일에. `.gitignore` 처리되어 git에 안 올라갑니다
- 권한 범위: `calendar.events`만 (캘린더 일정 읽기/쓰기). 메일·드라이브·연락처 다 못 봅니다
- 연결 해제: 명령 팔레트에서 같은 명령 → "연결 해제" 선택. 또는 [myaccount.google.com/permissions](https://myaccount.google.com/permissions)에서 직접 권한 회수
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""Google Calendar 자동 일정 등록 — secretary_calendar_write_v1.
이 스크립트는 OAuth와 실제 이벤트 생성을 직접 하지 않습니다 — VS Code
호스트(extension.ts)에서 직접 처리해요. 이 도구의 역할은:
1) 설정 상태를 확인해서 사용자에게 알려주기 (▶ 클릭 시)
2) ⚙️ 폼에서 CALENDAR_ID / DEFAULT_DURATION_MINUTES 같은 보조 설정 노출
연결 자체는 명령 팔레트에서:
Cmd+Shift+P → 'Connect AI: Google Calendar 자동 일정 연결 📅'
"""
import os, json, sys
HERE = os.path.dirname(os.path.abspath(__file__))
CONFIG = os.path.join(HERE, "google_calendar_write.json")
def main():
if not os.path.exists(CONFIG):
print("⚠️ 아직 설정이 없어요.")
print(" 명령 팔레트(Cmd+Shift+P) → 'Connect AI: Google Calendar 자동 일정 연결' 실행")
sys.exit(1)
try:
with open(CONFIG, "r", encoding="utf-8") as f:
cfg = json.load(f)
except Exception as e:
print(f"❌ 설정 파일 파싱 실패: {e}")
sys.exit(1)
cid = (cfg.get("CLIENT_ID") or "").strip()
cs = (cfg.get("CLIENT_SECRET") or "").strip()
rt = (cfg.get("REFRESH_TOKEN") or "").strip()
cal = (cfg.get("CALENDAR_ID") or "primary").strip()
dur = int(cfg.get("DEFAULT_DURATION_MINUTES") or 60)
who = (cfg.get("_CONNECTED_AS") or "").strip()
when = (cfg.get("_CONNECTED_AT") or "").strip()
print("─── Google Calendar 자동 일정 등록 상태 ───")
print(f" Client ID : {'설정됨 (' + cid[:8] + '…)' if cid else '(없음)'}")
print(f" Client Secret : {'설정됨' if cs else '(없음)'}")
print(f" Refresh Token : {'유효 ✓' if rt else '(없음)'}")
print(f" Calendar ID : {cal}")
print(f" 기본 일정 길이 : {dur}")
if who:
print(f" 연결 계정 : {who}")
if when:
print(f" 연결 시각 : {when[:19]}")
if not (cid and cs and rt):
print()
print("⚠️ 셋업이 완료되지 않았어요.")
print(" 명령 팔레트(Cmd+Shift+P) → 'Connect AI: Google Calendar 자동 일정 연결'")
sys.exit(1)
print()
print("✅ 연결 정상. 마감일(due) 있는 추적 작업이 등록되면 자동으로 캘린더에 일정이 생성됩니다.")
if __name__ == "__main__":
main()
@@ -0,0 +1,26 @@
# 📨 텔레그램 연결
비서(Secretary)가 텔레그램 메신저로 보고를 보내려면 봇 토큰과 chat_id가 필요해요. **⚙️ 버튼을 누르고 폼에 입력**하면 끝 — config.md를 열 필요 없습니다.
## 어떻게 도와주나요?
- ⚙️ 폼에 입력 → `telegram_setup.json`에 저장 (`.gitignore`로 git에서 제외)
- ▶ 실행 → 텔레그램에 연결 테스트 메시지 1발 발송
- 모든 에이전트(YouTube 도구 포함)가 이 설정을 자동으로 공유
## 봇 만드는 법 (한 번만, 약 2분)
1. 텔레그램에서 [@BotFather](https://t.me/BotFather) 검색 → `/newbot` 입력
2. 봇 이름·핸들 정하면 `123456789:ABC...` 형식 토큰을 줍니다 → ⚙️의 `TELEGRAM_BOT_TOKEN`에 입력
3. 새로 만든 봇한테 `/start` 같은 메시지 1번 보내기 (chat_id 활성화)
4. 브라우저에서 `https://api.telegram.org/bot<토큰>/getUpdates` 열어 `chat.id` 숫자 복사
5. ⚙️의 `TELEGRAM_CHAT_ID`에 입력 → 저장
6. ▶ 실행 → 텔레그램에서 "✅ 비서 연결 정상" 메시지 도착하면 끝
## 이 설정을 누가 사용하나?
- 비서 자체 (데일리 브리핑·할 일 알림 등)
- YouTube 도구 (내 영상 체크·경쟁 채널 분석 보고서 푸시)
- 향후 추가될 모든 에이전트의 텔레그램 알림
## 안전
- 토큰은 `.gitignore` 처리되어 GitHub에 안 올라갑니다
- 폼은 토큰 칸을 자동으로 password 형식으로 가립니다 (다른 사람 화면 공유해도 노출 X)
- 토큰 노출됐다 싶으면 [@BotFather](https://t.me/BotFather) → `/revoke`로 즉시 폐기 가능
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Telegram 연결 — secretary_telegram_v2.
Secretary 에이전트의 텔레그램 연결 도구. 토큰·chat_id를 Skills의 ⚙️ 폼에
입력하면 `telegram_setup.json`에 저장되고, 이 스크립트가 메시지 1발 보내서
연결을 테스트합니다. 회사의 모든 에이전트(YouTube 포함)가 이 설정을
공유합니다."""
import os, json, sys, time
HERE = os.path.dirname(os.path.abspath(__file__))
CONFIG = os.path.join(HERE, "telegram_setup.json")
def main():
if not os.path.exists(CONFIG):
print("❌ telegram_setup.json이 없어요. 먼저 ⚙️ 클릭해서 토큰을 입력해주세요.")
sys.exit(1)
try:
with open(CONFIG, "r", encoding="utf-8") as f:
cfg = json.load(f)
except Exception as e:
print(f"❌ 설정 파일 파싱 실패: {e}")
sys.exit(1)
token = (cfg.get("TELEGRAM_BOT_TOKEN") or "").strip()
chat = (cfg.get("TELEGRAM_CHAT_ID") or "").strip()
if not token or not chat:
print("❌ TELEGRAM_BOT_TOKEN 또는 TELEGRAM_CHAT_ID가 비어있어요.")
print(" 봇 만들기: Telegram → @BotFather → /newbot → 토큰 받기")
print(" chat_id : 봇에 메시지 1번 → https://api.telegram.org/bot<TOKEN>/getUpdates 에서 chat.id")
sys.exit(1)
try:
import requests
except ImportError:
print("❌ pip install requests")
sys.exit(1)
body = f"✅ 비서(Secretary) 텔레그램 연결 정상 — {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n이 메시지가 보이면 모든 에이전트가 이 채널로 보고를 보낼 수 있어요."
try:
r = requests.post(
f"https://api.telegram.org/bot{token}/sendMessage",
json={"chat_id": chat, "text": body, "parse_mode": "Markdown"},
timeout=15,
)
r.raise_for_status()
print(f"✅ 전송 OK — 텔레그램에서 확인하세요. ({len(body)}자)")
except Exception as e:
print(f"❌ 전송 실패: {e}")
if "Bad Request" in str(e):
print(" chat_id가 정확한지, 봇과 한 번이라도 대화를 시작했는지 확인하세요.")
sys.exit(1)
if __name__ == "__main__":
main()
+15
View File
@@ -0,0 +1,15 @@
# ✍️ Writer 에이전트 — 나의 미션
> 🌞 24시간 업무가 켜져 있으면 이 미션을 향해 자동으로 한 스텝씩 일합니다.
> 자유롭게 수정하세요. 비워두면 회사 공동 목표만 따라갑니다.
## 장기 목표 (3~6개월)
- 후크·CTA 라이브러리 50개 운영
- 채널·인스타·블로그 톤앤매너 가이드 확정
## 이번 주 목표
- 영상 스크립트 초안 2편 (후크 3안 포함)
- 인스타 캡션 5개 + 블로그 글 1편
## 작업 원칙
- 한 산출물에 후크/본문/CTA를 명확히 분리
+5
View File
@@ -0,0 +1,5 @@
# ✍️ Writer (Copywriter) 개인 메모리
_Writer 에이전트만 읽고 쓰는 개인 노트. 학습·교훈·자주 쓰는 패턴이 누적됩니다._
## 학습 기록
+5
View File
@@ -0,0 +1,5 @@
# ✍️ Writer 페르소나 디테일
_여기에 Writer 에이전트에게 주고 싶은 추가 지시·말투·취향·예시 등을 자유롭게 적으세요._
_매 호출 시 시스템 프롬프트에 자동 주입됩니다. (git에 동기화됨)_
+55
View File
@@ -0,0 +1,55 @@
# ✍️ Writer — 도구 매니페스트
_Writer 에이전트가 어떤 도구를 어디까지 자율적으로 쓸 수 있는지 정의합니다._
_매번 시스템 프롬프트로 주입되며, 텔레그램에서 `/tools`로 현재 상태 확인 가능._
---
## 자율도 레벨
AUTONOMY_LEVEL: 2
| 값 | 의미 |
|---|---|
| 0 | Off — 도구 전체 비활성 (이 에이전트는 채팅만) |
| 1 | Read-only — 읽기·분석·보고만, 외부에 쓰기 X |
| 2 | Draft — 초안 작성 후 사용자 승인 게이트 통과해야 실행 ⭐ 권장 기본값 |
| 3 | Auto — 화이트리스트 안에서 사용자 승인 없이 실행 |
> 위 `AUTONOMY_LEVEL` 줄의 숫자(0~3)를 직접 바꾸면 다음 호출부터 적용됩니다.
---
## 사용 가능한 도구
### `tone_learner`
사용자 과거 글 학습 → 톤 복제
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `multi_platform_adapt`
하나의 스크립트 → YouTube/IG/블로그 자동 변환
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `hook_library`
후크·CTA 라이브러리 운영
- `enabled`: true
- `requires_credentials`: `config.md` 참조
---
## 안전 규칙 (모든 레벨 공통, 절대 우회 X)
- **삭제·배포·발송**(rm, deploy --prod, send, publish) 류는 자율도와 무관하게 **항상 승인 게이트**.
- 외부 API 호출 전 `config.md`의 토큰 존재 여부 확인.
- 모든 외부 행동은 `_agents/writer/activity.log`에 한 줄 기록 (감사용).
- 승인 대기 액션은 `approvals/pending/` 에 저장 → 텔레그램 `/approvals` 로 조회.
---
_레벨을 어떻게 골라야 할지 모르겠다면 `2 (Draft)`가 안전한 시작점입니다._
+27
View File
@@ -0,0 +1,27 @@
# 🎯 YouTube 에이전트 — 나의 미션
> 🌞 24시간 업무가 켜져 있으면 이 미션을 향해 자동으로 한 스텝씩 일합니다.
> 자유롭게 수정하세요. 비워두면 회사 공동 목표만 따라갑니다.
## 장기 목표 (3~6개월)
- 채널 정체성 확립 + 구독자 1만 도달
- 영상 평균 시청 지속률 50% 이상
## 이번 주 목표
- 후크 강한 영상 기획서 3개 작성
- 감시 채널 댓글 패턴에서 후크 단어 5개 추출
- 경쟁 채널 인기 영상 → 다음 액션 브리프 1건
## 사용 가능한 도구 (Skills)
- 🔑 `youtube_account` — API 키·내 채널·감시 채널·텔레그램 한 번에 설정
- 🎯 `trend_sniper` — 키워드 기반 떡상 영상 패턴 분석
- 🌙 `auto_planner` — 트렌드 스나이퍼 무인 반복 실행
- 🎬 `my_videos_check` — 내 채널 영상이 잘 올라갔는지 자동 판단
- 💬 `comment_harvester` — 감시 채널 댓글 → memory.md 누적
- 🔭 `competitor_brief` — 경쟁 채널 → 지시문 형식 다음 액션
- 📨 `telegram_notify` — 다른 도구 보고를 메신저로 자동 푸시
## 작업 원칙
- 추상적 조언 대신 **실행 가능한 산출물** (제목·썸네일 브리프·스크립트 후크)
- 매번 다음 단계 1줄을 명시
- 메모리(`memory.md`)에 누적된 댓글·반응 키워드를 후크에 반영
+5
View File
@@ -0,0 +1,5 @@
# 📺 YouTube (Head of YouTube) 개인 메모리
_YouTube 에이전트만 읽고 쓰는 개인 노트. 학습·교훈·자주 쓰는 패턴이 누적됩니다._
## 학습 기록
+5
View File
@@ -0,0 +1,5 @@
# 📺 YouTube 페르소나 디테일
_여기에 YouTube 에이전트에게 주고 싶은 추가 지시·말투·취향·예시 등을 자유롭게 적으세요._
_매 호출 시 시스템 프롬프트에 자동 주입됩니다. (git에 동기화됨)_
+1
View File
@@ -0,0 +1 @@
self-rag
+67
View File
@@ -0,0 +1,67 @@
# 📺 레오 — 도구 매니페스트
_레오 에이전트가 어떤 도구를 어디까지 자율적으로 쓸 수 있는지 정의합니다._
_매번 시스템 프롬프트로 주입되며, 텔레그램에서 `/tools`로 현재 상태 확인 가능._
---
## 자율도 레벨
AUTONOMY_LEVEL: 2
| 값 | 의미 |
|---|---|
| 0 | Off — 도구 전체 비활성 (이 에이전트는 채팅만) |
| 1 | Read-only — 읽기·분석·보고만, 외부에 쓰기 X |
| 2 | Draft — 초안 작성 후 사용자 승인 게이트 통과해야 실행 ⭐ 권장 기본값 |
| 3 | Auto — 화이트리스트 안에서 사용자 승인 없이 실행 |
> 위 `AUTONOMY_LEVEL` 줄의 숫자(0~3)를 직접 바꾸면 다음 호출부터 적용됩니다.
---
## 사용 가능한 도구
### `youtube_account`
YouTube Data API v3 OAuth 연결
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `comment_replier`
댓글 분류 + 답글 초안 (Draft 레벨에서 동작)
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `video_uploader`
제목·태그·썸네일·예약발행 업로드
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `analytics_pull`
주간 인사이트 (조회수·시청 지속률·구독 전환)
- `enabled`: true
- `requires_credentials`: `config.md` 참조
### `trend_sniper`
내 분야 트렌드 → Writer에게 아이디어 전달
- `enabled`: true
- `requires_credentials`: `config.md` 참조
---
## 안전 규칙 (모든 레벨 공통, 절대 우회 X)
- **삭제·배포·발송**(rm, deploy --prod, send, publish) 류는 자율도와 무관하게 **항상 승인 게이트**.
- 외부 API 호출 전 `config.md`의 토큰 존재 여부 확인.
- 모든 외부 행동은 `_agents/youtube/activity.log`에 한 줄 기록 (감사용).
- 승인 대기 액션은 `approvals/pending/` 에 저장 → 텔레그램 `/approvals` 로 조회.
---
_레벨을 어떻게 골라야 할지 모르겠다면 `2 (Draft)`가 안전한 시작점입니다._
@@ -0,0 +1,29 @@
# 🌙 오토 플래너
트렌드 스나이퍼를 정해진 간격으로 반복 실행해서 패턴 데이터를 쌓아주는 무인 작업자예요. 한 번 트렌드를 보면 지금 잘 되는 영상 한 장만 보이지만, 8시간 동안 2시간마다 4번 보면 "어떤 키워드의 후크가 시간이 지나도 계속 살아남는지"가 보이기 시작합니다 — 자는 동안에 그 작업을 대신해줍니다.
## 어떻게 도와주나요?
- ⏰ N시간마다 `trend_sniper.py`를 자동 실행 (스나이퍼 결과는 매번 sessions/에 누적)
- 🛌 잘 때 켜두면 아침에 4~5번분의 트렌드 스냅샷이 쌓여 있어요
- 📊 같은 키워드라도 시간대별로 어떤 영상이 새로 떠오르는지 비교 가능
## 어떤 상황에 켜면 좋나요?
- 새 채널 컨셉을 결정하기 전, 며칠치 트렌드를 누적해서 보고 싶을 때
- 회사 일/외출 중 백그라운드에서 데이터만 모아두고 싶을 때
- 특정 키워드의 알고리즘 반응이 시간대마다 다른지 확인하고 싶을 때
## 시작하기 전 체크
- 트렌드 스나이퍼 도구가 먼저 설정돼 있어야 해요 (YouTube API 키, 키워드 목록 등)
- 첫 실행 전에 트렌드 스나이퍼를 한 번 수동으로 돌려서 정상 작동 확인을 권장합니다
## 설정값 (auto_planner.json)
- `INTERVAL_HOURS` — 몇 시간마다 실행할지 (기본 2)
- `TOTAL_RUN_HOURS` — 총 가동 시간 (기본 8 → 8시간 동안 4회 실행)
## 실행 방법
패널의 [▶ 실행]을 누르면 시작됩니다. 또는 터미널에서:
```bash
python auto_planner.py
```
⚠️ 이 스크립트는 끝날 때까지 터미널을 점유해요. 백그라운드로 돌리려면 별도 창에서 실행하세요. 중단하려면 Ctrl+C.
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""Auto Planner — runs trend_sniper.py on a fixed interval for a chosen
duration (e.g. overnight). Reads its config from auto_planner.json."""
import os, json, time, datetime, subprocess, sys
HERE = os.path.dirname(os.path.abspath(__file__))
CONFIG_PATH = os.path.join(HERE, "auto_planner.json")
SNIPER_PATH = os.path.join(HERE, "trend_sniper.py")
def load_config():
try:
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
print(f"❌ 설정 파일을 읽을 수 없어요: {CONFIG_PATH}\n{e}")
sys.exit(1)
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}시간마다 트렌드 분석 실행")
if not os.path.exists(SNIPER_PATH):
print(f"❌ trend_sniper.py를 찾을 수 없어요: {SNIPER_PATH}")
sys.exit(1)
start = time.time()
loop = 0
while True:
if 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}회차 트렌드 스나이핑")
try:
subprocess.run([sys.executable, SNIPER_PATH], check=False)
except Exception as e:
print(f"❌ 실행 실패: {e}")
print(f"⏳ 다음 실행: {interval_h}시간 후")
time.sleep(interval_h * 3600)
if __name__ == "__main__":
main()
@@ -0,0 +1,18 @@
# 📈 채널 완전 분석
본인 YouTube 채널을 한 번에 깊이있게 진단합니다. 추가 입력 없이 외부 연결 패널의 API 키 + 채널 ID만 있으면 즉시 작동.
## 무엇을 분석하나요?
- **채널 개요** — 구독자·총 조회수·영상 수·평균 조회수
- **업로드 패턴** — 최근 30일 업로드 횟수·요일·영상 길이
- **성과 통계** — 중간값/평균 조회수, 평균 참여율
- **떡상 vs 부진 비교** — 인기 영상과 부진 영상의 제목·길이 패턴 차이
- **자동 추천** — 데이터 기반 다음 액션 (LLM 호출 없이 통계만으로)
## 입력
`youtube_account.json``YOUTUBE_API_KEY` + `MY_CHANNEL_HANDLE` 또는 `MY_CHANNEL_ID` (외부 연결 패널에서 1회 입력하면 끝)
## 출력
- 콘솔에 8개 섹션 보고서
- `channel_full_analysis_report.md`에 누적 저장
- (선택) 텔레그램 자동 알림
@@ -0,0 +1,304 @@
#!/usr/bin/env python3
"""Channel Full Analysis — comprehensive overview of your YouTube channel.
Input: just YOUTUBE_API_KEY + MY_CHANNEL_ID/HANDLE from youtube_account.json.
No additional config needed. Output: full report with stats, patterns, and
data-driven recommendations.
"""
import os, json, sys, time, datetime, statistics, re
from collections import Counter
HERE = os.path.dirname(os.path.abspath(__file__))
ACCOUNT = os.path.join(HERE, "youtube_account.json")
REPORT = os.path.join(HERE, "channel_full_analysis_report.md")
def _load(p):
with open(p, "r", encoding="utf-8") as f:
return json.load(f)
def _resolve_channel_id(youtube, handle, channel_id):
if channel_id:
return channel_id
if not handle:
return None
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"]
except Exception as e:
print(f"⚠️ 채널 ID 조회 실패: {e}")
return None
def _parse_iso_duration(d):
"""ISO 8601 duration (PT4M30S) → seconds."""
m = re.match(r"PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?", d or "")
if not m: return 0
h, mi, s = m.groups()
return int(h or 0) * 3600 + int(mi or 0) * 60 + int(s or 0)
def _fmt_duration(sec):
if sec < 60: return f"{sec}s"
if sec < 3600: return f"{sec//60}m {sec%60}s"
return f"{sec//3600}h {(sec%3600)//60}m"
def _resolve_telegram(account):
"""Same fallback chain as my_videos_check.py."""
import json as _json
token = (account.get("TELEGRAM_BOT_TOKEN") or "").strip()
chat = (account.get("TELEGRAM_CHAT_ID") or "").strip()
if token and chat:
return token, chat
brain_root = os.path.abspath(os.path.join(HERE, "..", "..", ".."))
sec_json = os.path.join(brain_root, "_agents", "secretary", "tools", "telegram_setup.json")
if (not token or not chat) and os.path.exists(sec_json):
try:
with open(sec_json, "r", encoding="utf-8") as f:
cfg = _json.load(f)
if not token: token = (cfg.get("TELEGRAM_BOT_TOKEN") or "").strip()
if not chat: chat = (cfg.get("TELEGRAM_CHAT_ID") or "").strip()
except Exception:
pass
return token, chat
def _push_telegram(account, text):
token, chat = _resolve_telegram(account)
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, "parse_mode": "Markdown"},
timeout=10,
)
print("📨 텔레그램으로 보고 전송")
except Exception as e:
print(f"⚠️ 텔레그램 전송 실패: {e}")
def main():
if not os.path.exists(ACCOUNT):
print("❌ youtube_account.json이 없어요. 외부 연결 패널에서 YouTube API 키와 채널 ID 입력해주세요.")
sys.exit(1)
acct = _load(ACCOUNT)
api_key = (acct.get("YOUTUBE_API_KEY") or "").strip()
handle = (acct.get("MY_CHANNEL_HANDLE") or "").strip()
chan_id = (acct.get("MY_CHANNEL_ID") or "").strip()
if not api_key:
print("❌ YOUTUBE_API_KEY가 비어있어요. 외부 연결 패널 → YouTube Data API 카드에 입력해주세요.")
sys.exit(1)
if not (handle or chan_id):
print("❌ MY_CHANNEL_HANDLE 또는 MY_CHANNEL_ID 필요. 외부 연결 패널 → 채널 ID 입력해주세요.")
sys.exit(1)
try:
from googleapiclient.discovery import build
except ImportError:
print("❌ google-api-python-client 미설치.")
print(" 터미널에서 한 줄: pip3 install google-api-python-client requests")
sys.exit(1)
youtube = build("youtube", "v3", developerKey=api_key)
cid = _resolve_channel_id(youtube, handle, chan_id)
if not cid:
print("❌ 채널 ID를 찾지 못했어요. 외부 연결 패널의 채널 ID 확인.")
sys.exit(1)
print(f"📈 [채널 완전 분석] 채널 {handle or cid} 분석 중...")
print()
# 1. 채널 메타
ch = youtube.channels().list(part="snippet,statistics,brandingSettings", id=cid).execute()
if not ch.get("items"):
print("❌ 채널 데이터를 가져오지 못했어요. API 키·할당량 확인.")
sys.exit(1)
c = ch["items"][0]
sn = c.get("snippet", {})
st = c.get("statistics", {})
title = sn.get("title", "(이름 없음)")
subs = int(st.get("subscriberCount", 0))
total_views = int(st.get("viewCount", 0))
video_count = int(st.get("videoCount", 0))
pub_at = sn.get("publishedAt", "")[:10]
print("─── 1. 채널 개요 ───")
print(f" 채널: {title}")
print(f" 핸들: {sn.get('customUrl', handle or '(없음)')}")
print(f" 구독자: {subs:,}")
print(f" 총 조회수: {total_views:,}")
print(f" 업로드 영상: {video_count}")
print(f" 채널 가입: {pub_at}")
avg_per_video = total_views // max(1, video_count)
print(f" 영상당 평균 조회: {avg_per_video:,}")
print()
# 2. 최근 30일 영상 분석 (uploads playlist 사용 — search보다 quota 절약)
uploads = c.get("contentDetails", {}).get("relatedPlaylists", {}).get("uploads") if "contentDetails" in c else None
if not uploads:
# contentDetails 없으면 search로 폴백
cd = youtube.channels().list(part="contentDetails", id=cid).execute()
if cd.get("items"):
uploads = cd["items"][0]["contentDetails"]["relatedPlaylists"]["uploads"]
cutoff = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=30)
recent_video_ids = []
if uploads:
next_token = None
while len(recent_video_ids) < 50:
args = {"part": "snippet,contentDetails", "playlistId": uploads, "maxResults": 50}
if next_token: args["pageToken"] = next_token
pi = youtube.playlistItems().list(**args).execute()
for item in pi.get("items", []):
pub = item["snippet"]["publishedAt"]
pub_dt = datetime.datetime.fromisoformat(pub.replace("Z", "+00:00"))
if pub_dt < cutoff:
break
recent_video_ids.append(item["contentDetails"]["videoId"])
next_token = pi.get("nextPageToken")
if not next_token: break
if recent_video_ids and datetime.datetime.fromisoformat(pi["items"][-1]["snippet"]["publishedAt"].replace("Z", "+00:00")) < cutoff:
break
if not recent_video_ids:
print("⚠️ 최근 30일 동안 업로드한 영상이 없어요. 영상 업로드 후 다시 분석해주세요.")
sys.exit(0)
# 3. 영상별 통계 (50개씩 나눠서)
all_vids = []
for i in range(0, len(recent_video_ids), 50):
chunk = recent_video_ids[i:i+50]
st_resp = youtube.videos().list(part="snippet,statistics,contentDetails", id=",".join(chunk)).execute()
for v in st_resp.get("items", []):
stats = v.get("statistics", {})
sn_v = v.get("snippet", {})
cd_v = v.get("contentDetails", {})
views = int(stats.get("viewCount", 0))
likes = int(stats.get("likeCount", 0))
comments = int(stats.get("commentCount", 0))
duration_sec = _parse_iso_duration(cd_v.get("duration", ""))
pub = sn_v.get("publishedAt", "")
pub_dt = datetime.datetime.fromisoformat(pub.replace("Z", "+00:00"))
all_vids.append({
"id": v["id"],
"title": sn_v.get("title", ""),
"views": views,
"likes": likes,
"comments": comments,
"duration_sec": duration_sec,
"pub_dt": pub_dt,
"engagement_rate": (likes + comments) / views if views > 0 else 0,
})
all_vids.sort(key=lambda x: x["views"], reverse=True)
views_list = [v["views"] for v in all_vids]
median_views = statistics.median(views_list) if views_list else 0
mean_views = statistics.mean(views_list) if views_list else 0
print("─── 2. 최근 30일 업로드 패턴 ───")
print(f" 업로드 횟수: {len(all_vids)}개 (월평균 {len(all_vids):.1f}개)")
weekday_counts = Counter(v["pub_dt"].strftime("%A") for v in all_vids)
weekday_kr = {"Monday":"","Tuesday":"","Wednesday":"","Thursday":"","Friday":"","Saturday":"","Sunday":""}
top_day = weekday_counts.most_common(1)
if top_day:
print(f" 주로 업로드한 요일: {weekday_kr.get(top_day[0][0], top_day[0][0])}요일 ({top_day[0][1]}회)")
avg_duration = sum(v["duration_sec"] for v in all_vids) / len(all_vids)
print(f" 평균 영상 길이: {_fmt_duration(int(avg_duration))}")
print()
print("─── 3. 성과 통계 ───")
print(f" 중간값 조회수: {int(median_views):,}")
print(f" 평균 조회수: {int(mean_views):,}")
avg_eng = sum(v["engagement_rate"] for v in all_vids) / len(all_vids) * 100 if all_vids else 0
print(f" 평균 참여율 (좋아요+댓글)/조회: {avg_eng:.2f}%")
print()
# 떡상 / 부진 분류
hot = [v for v in all_vids if v["views"] >= median_views * 1.5]
cold = [v for v in all_vids if v["views"] < median_views * 0.5]
print("─── 4. 🔥 떡상 영상 (중간값 × 1.5 이상) ───")
if not hot:
print(" (없음 — 모든 영상이 평균 근처)")
else:
for v in hot[:5]:
print(f" 🔥 {v['views']:>8,}회 · 참여 {v['engagement_rate']*100:.2f}% · {_fmt_duration(v['duration_sec'])} · {v['title'][:50]}")
print()
print("─── 5. 🥶 부진 영상 (중간값 × 0.5 미만) ───")
if not cold:
print(" (없음 — 모든 영상이 평균 근처)")
else:
for v in cold[:5]:
print(f" 🥶 {v['views']:>8,}회 · 참여 {v['engagement_rate']*100:.2f}% · {_fmt_duration(v['duration_sec'])} · {v['title'][:50]}")
print()
# 6. 패턴 비교 — 떡상 vs 부진의 차이
print("─── 6. 떡상 vs 부진 — 패턴 비교 ───")
if hot and cold:
hot_avg_dur = sum(v["duration_sec"] for v in hot) / len(hot)
cold_avg_dur = sum(v["duration_sec"] for v in cold) / len(cold)
hot_avg_title = sum(len(v["title"]) for v in hot) / len(hot)
cold_avg_title = sum(len(v["title"]) for v in cold) / len(cold)
print(f" 떡상 영상 평균 길이: {_fmt_duration(int(hot_avg_dur))}")
print(f" 부진 영상 평균 길이: {_fmt_duration(int(cold_avg_dur))}")
if abs(hot_avg_dur - cold_avg_dur) > 60:
longer = "떡상" if hot_avg_dur > cold_avg_dur else "부진"
print(f"{longer} 영상이 평균 {abs(int(hot_avg_dur - cold_avg_dur))}초 더 길어요")
print(f" 떡상 영상 평균 제목 길이: {hot_avg_title:.0f}")
print(f" 부진 영상 평균 제목 길이: {cold_avg_title:.0f}")
else:
print(" (떡상 또는 부진 데이터 부족 — 영상이 더 쌓이면 다시 분석)")
print()
# 7. 자동 추천 (LLM 없이 데이터만으로)
print("─── 7. 🧭 다음 액션 추천 (데이터 기반) ───")
actions = []
if hot:
actions.append(f"🔥 떡상한 {len(hot)}개 영상의 제목·후크 패턴을 다음 영상에 적용 — 가장 잘 된 후크는 \"{hot[0]['title'][:50]}\"")
if cold:
actions.append(f"🥶 부진한 {len(cold)}개는 썸네일 A/B 테스트 또는 제목 리네이밍 후보")
if avg_eng < 2.0:
actions.append(f"💗 평균 참여율 {avg_eng:.2f}% — 영상 끝에 명확한 CTA(좋아요·구독) 추가 추천 (보통 3% 이상이 건강함)")
elif avg_eng > 5.0:
actions.append(f"💗 참여율 {avg_eng:.2f}% — 매우 좋음. 시청자와 강한 연결 구축됨, 상품·멤버십 도입 고려 시점")
if len(all_vids) < 4:
actions.append("📅 월 4개 미만 업로드 — 알고리즘 노출 위해 최소 주 1회 권장")
elif len(all_vids) > 12:
actions.append("📅 월 12개 이상 업로드 — 양은 충분, 영상별 품질·후크에 집중 추천")
if not actions:
actions.append("✅ 채널 상태 안정적 — 현재 패턴 유지하며 시청자 댓글에서 다음 콘텐츠 아이디어 수집")
for a in actions:
print(f"{a}")
print()
# 8. 보고서 .md 저장
summary_lines = [
f"# 📈 채널 완전 분석 — {time.strftime('%Y-%m-%d %H:%M')}",
f"채널: **{title}** · 구독자 **{subs:,}** · 영상 **{video_count}**개",
"",
"## 최근 30일 통계",
f"- 업로드: {len(all_vids)}",
f"- 조회수 중간값: **{int(median_views):,}**",
f"- 평균 참여율: **{avg_eng:.2f}%**",
f"- 평균 영상 길이: **{_fmt_duration(int(avg_duration))}**",
"",
f"## 🔥 떡상 영상 ({len(hot)}개)",
]
for v in hot[:5]:
summary_lines.append(f"- {v['views']:,}회 · {v['title']}")
summary_lines.append(f"\n## 🥶 부진 영상 ({len(cold)}개)")
for v in cold[:5]:
summary_lines.append(f"- {v['views']:,}회 · {v['title']}")
summary_lines.append("\n## 🧭 다음 액션 (자동 추천)")
for a in actions:
summary_lines.append(f"- {a}")
summary = "\n".join(summary_lines)
with open(REPORT, "a", encoding="utf-8") as f:
f.write("\n\n" + summary + "\n\n---\n")
print(f"✅ 보고서: {REPORT}")
_push_telegram(acct, summary)
if __name__ == "__main__":
main()
@@ -0,0 +1,21 @@
# 💬 댓글 수집기
`youtube_account.json``WATCHED_CHANNELS`에 적은 채널들의 최근 영상에서 인기 댓글을 가져와 YouTube 에이전트의 `memory.md`에 누적 저장합니다. 시청자가 실제로 어떤 단어·반응을 쓰는지가 메모리에 쌓이면, 에이전트가 다음 영상 후크나 제목을 짤 때 그 표현을 자연스럽게 참고하게 됩니다.
## 어떻게 도와주나요?
- 📡 감시 채널마다 최근 N개 영상 → 인기 댓글 M개 가져오기
- 🧠 결과를 `_agents/youtube/memory.md`에 자동 추가 (에이전트가 다음 사이클에 자동 참조)
- 📒 같은 폴더에 `comment_harvester_report.md`로 누적 백업
## 시작하기 전 체크
- `youtube_account.json``WATCHED_CHANNELS` 배열 채워두기 (예: `["@channel_a","@channel_b"]`)
- 댓글이 꺼진 영상은 자동 스킵
- API 비용: 채널당 search 1회 + 영상마다 commentThreads 1회 (가벼움)
## 설정값 (comment_harvester.json)
- `VIDEOS_PER_CHANNEL` — 채널마다 영상 몇 개 (기본 5)
- `COMMENTS_PER_VIDEO` — 영상마다 댓글 몇 개 (기본 20)
- `LOOKBACK_DAYS` — 며칠치 영상까지 (기본 14)
## 어떻게 활용되나?
메모리에 쌓인 댓글을 에이전트가 다음 한 스텝에서 자연스럽게 참고합니다. 직접 보고 싶으면 `memory.md` 또는 같은 폴더의 `comment_harvester_report.md`를 열면 돼요.
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""Comment Harvester — for every channel in WATCHED_CHANNELS, pulls the most
recent N videos and their top M comments. Appends the results to the agent's
memory.md so the YouTube agent can reference real audience reactions on the
next think step.
Reads from youtube_account.json (api key, watched channels) and
comment_harvester.json (volume settings)."""
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, "comment_harvester.json")
# memory.md lives one level up — under _agents/youtube/
MEMORY = os.path.abspath(os.path.join(HERE, "..", "memory.md"))
REPORT = os.path.join(HERE, "comment_harvester_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 as e:
print(f"⚠️ {handle} 채널 조회 실패: {e}")
return None, None
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()
watched = acct.get("WATCHED_CHANNELS") or []
if not api_key:
print("❌ YOUTUBE_API_KEY 비어있음.")
sys.exit(1)
if not watched:
print("❌ WATCHED_CHANNELS가 비어있어요. youtube_account.json에 핸들 목록을 넣어주세요.")
print(' 예: "WATCHED_CHANNELS": ["@channel_a", "@channel_b"]')
sys.exit(1)
vids_per = int(cfg.get("VIDEOS_PER_CHANNEL", 5))
cmts_per = int(cfg.get("COMMENTS_PER_VIDEO", 20))
lookback = int(cfg.get("LOOKBACK_DAYS", 14))
try:
from googleapiclient.discovery import build
except ImportError:
print("❌ pip install google-api-python-client")
sys.exit(1)
youtube = build("youtube", "v3", developerKey=api_key)
after = (datetime.datetime.utcnow() - datetime.timedelta(days=lookback)).isoformat("T") + "Z"
harvested = []
for ch in watched:
cid, ctitle = _resolve_channel_id(youtube, ch)
if not cid:
continue
print(f"📡 [{ch}] 최근 영상 {vids_per}개 가져오는 중...")
sr = youtube.search().list(part="snippet", channelId=cid, maxResults=vids_per,
order="date", publishedAfter=after, type="video").execute()
for it in sr.get("items", []):
vid = it["id"]["videoId"]
vtitle = it["snippet"]["title"]
print(f" 💬 {vtitle[:60]}")
try:
cr = youtube.commentThreads().list(part="snippet", videoId=vid,
maxResults=cmts_per, order="relevance",
textFormat="plainText").execute()
except Exception as e:
msg = str(e)
if "commentsDisabled" in msg or "disabled" in msg.lower():
continue
print(f" ⚠️ 댓글 가져오기 실패: {e}")
continue
comments = []
for ci in cr.get("items", []):
top = ci["snippet"]["topLevelComment"]["snippet"]
comments.append({
"author": top.get("authorDisplayName", ""),
"likes": int(top.get("likeCount", 0)),
"text": (top.get("textDisplay", "") or "")[:280],
})
harvested.append({
"channel": ch, "channel_title": ctitle,
"video": vtitle, "video_id": vid, "comments": comments,
})
if not harvested:
print("⚠️ 수집된 댓글 없음.")
sys.exit(0)
ts = time.strftime('%Y-%m-%d %H:%M')
md_lines = [f"\n## 💬 시청자 댓글 수집 — {ts}"]
for h in harvested:
md_lines.append(f"\n### {h['channel_title']} ({h['channel']}) — {h['video']}")
md_lines.append(f"https://youtu.be/{h['video_id']}")
for c in h["comments"][:10]:
md_lines.append(f"- ({c['likes']}❤) **{c['author']}**: {c['text']}")
block = "\n".join(md_lines)
# Append to memory so the agent uses these comments next think.
os.makedirs(os.path.dirname(MEMORY), exist_ok=True)
if not os.path.exists(MEMORY):
with open(MEMORY, "w", encoding="utf-8") as f:
f.write("# YouTube 에이전트 — 메모리\n\n")
with open(MEMORY, "a", encoding="utf-8") as f:
f.write("\n" + block + "\n")
with open(REPORT, "a", encoding="utf-8") as f:
f.write("\n" + block + "\n\n---\n")
print(f"\n✅ 메모리에 추가: {MEMORY}")
print(f"✅ 보고서: {REPORT}")
print(f" {len(harvested)}개 영상 · 평균 {sum(len(h['comments']) for h in harvested)//max(len(harvested),1)}개 댓글")
if __name__ == "__main__":
main()
@@ -0,0 +1,20 @@
# 🔭 경쟁 채널 분석
`youtube_account.json``COMPETITOR_CHANNELS`에 적은 경쟁 채널들의 최근 떡상 영상을 모아서, 로컬 LLM에게 **지시문 형식**의 다음 액션 브리프를 받아옵니다 — "이거 해야합니다 / 저거 해야합니다 / 이건 절대 하지 마세요" 형태로 나옵니다.
## 어떻게 도와주나요?
- 🔭 경쟁 채널마다 최근 N개 인기 영상(view 기준) 수집
- 🧠 로컬 LLM이 패턴을 읽고 4섹션으로 브리프 작성:
- 1) 지금 당장 해야 하는 것 3개
- 2) 이번 주 시도할 것 3개 (제목 후보 포함)
- 3) 절대 하지 말 것 1개
- 4) 다음 영상 핵심 한 줄
- 📨 텔레그램 설정돼있으면 자동 푸시
## 시작하기 전 체크
- `youtube_account.json``COMPETITOR_CHANNELS` 채워두기
- 로컬 LLM(Ollama/LM Studio)이 켜져 있어야 함
## 설정값 (competitor_brief.json)
- `TOP_N_PER_CHANNEL` — 채널마다 상위 영상 몇 개 (기본 5)
- `LOOKBACK_DAYS` — 며칠치 (기본 30)
@@ -0,0 +1,156 @@
#!/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()
@@ -0,0 +1,22 @@
# 📊 내 유튜브 채널 분석
본인 채널의 최근 영상이 잘 올라갔는지 한눈에 봅니다. 조회수 중간값을 기준선으로 삼아 떡상/부진 영상을 자동 분류하고, 다음에 뭘 할지 짧은 제안까지 만들어줘요.
## 어떻게 도와주나요?
- 🎬 본인 채널 최근 N개 영상 메타·통계 수집
- 📊 조회수 **중간값** 계산 → 1.5배 이상 = 🔥 떡상, 0.5배 미만 = 🥶 부진
- 🧭 떡상/부진 비율 보고 다음 액션 1~3개 제안
- 📨 `youtube_account.json`에 텔레그램이 설정돼있으면 보고를 메시지로도 보내줌
## 시작하기 전 체크
- `youtube_account.json``YOUTUBE_API_KEY` + `MY_CHANNEL_HANDLE` 또는 `MY_CHANNEL_ID` 채워야 함
- 핸들만 있어도 자동으로 채널 ID를 조회합니다 (검색 1회 사용)
## 설정값 (my_videos_check.json)
- `LOOKBACK_DAYS` — 며칠치 영상 볼지 (기본 30)
- `TOP_N` — 최대 몇 개 분석할지 (기본 10)
## 출력
- 콘솔에 영상별 조회수·라이크·댓글 수
- `my_videos_check_report.md`에 누적 저장
- (선택) 텔레그램 알림
@@ -0,0 +1,478 @@
#!/usr/bin/env python3
"""Professional YouTube Channel Analysis — pro_v4.
채널 메타 · 영상별 상세 (조회수·좋아요율·댓글율·길이·요일) · 상위/하위 영상의 패턴 ·
인기 댓글 샘플 · 발행 요일 분석 · 제목 키워드 · 우선순위 액션 추천. 모든 분석은
실제 YouTube Data API 호출 결과 기반.
Reads YOUTUBE_API_KEY + MY_CHANNEL_HANDLE/ID from youtube_account.json.
Reads LOOKBACK_DAYS / TOP_N / COMMENT_SAMPLES from my_videos_check.json."""
import os, json, sys, time, datetime, re, statistics, warnings, html as html_lib
from collections import Counter
# v2.89.49 — DeprecationWarning(utcnow 등) 노이즈 제거. 사용자 채팅창 출력에 끼면 못생김.
warnings.filterwarnings("ignore", category=DeprecationWarning)
HERE = os.path.dirname(os.path.abspath(__file__))
ACCOUNT = os.path.join(HERE, "youtube_account.json")
CONFIG = os.path.join(HERE, "my_videos_check.json")
REPORT = os.path.join(HERE, "my_videos_check_report.md")
def _load(p):
with open(p, "r", encoding="utf-8") as f:
return json.load(f)
def _resolve_channel_id(youtube, handle, channel_id):
if channel_id:
return channel_id
if not handle:
return None
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"]
except Exception as e:
print(f"⚠️ 채널 ID 조회 실패: {e}")
return None
def _resolve_telegram(account):
"""telegram_v3 — Secretary's tools/telegram_setup.json is the canonical
UI-managed home (input via Skills ⚙️). Fallback chain:
1) youtube_account.json (this tool's local override, back-compat)
2) _agents/secretary/tools/telegram_setup.json (UI-managed, canonical)
3) _agents/secretary/config.md (legacy markdown, back-compat)
"""
import re, json as _json
token = (account.get("TELEGRAM_BOT_TOKEN") or "").strip()
chat = (account.get("TELEGRAM_CHAT_ID") or "").strip()
if token and chat:
return token, chat
brain_root = os.path.abspath(os.path.join(HERE, "..", "..", ".."))
# 2) Secretary's tool JSON
sec_json = os.path.join(brain_root, "_agents", "secretary", "tools", "telegram_setup.json")
if (not token or not chat) and os.path.exists(sec_json):
try:
with open(sec_json, "r", encoding="utf-8") as f:
cfg = _json.load(f)
if not token: token = (cfg.get("TELEGRAM_BOT_TOKEN") or "").strip()
if not chat: chat = (cfg.get("TELEGRAM_CHAT_ID") or "").strip()
except Exception:
pass
# 3) Legacy config.md
sec_cfg = os.path.join(brain_root, "_agents", "secretary", "config.md")
if (not token or not chat) and os.path.exists(sec_cfg):
try:
with open(sec_cfg, "r", encoding="utf-8") as f:
txt = f.read()
if not token:
m = re.search(r"TELEGRAM_BOT_TOKEN\s*[:=]\s*([A-Za-z0-9:_\-]+)", txt)
if m: token = m.group(1).strip()
if not chat:
m = re.search(r"TELEGRAM_CHAT_ID\s*[:=]\s*(-?\d+)", txt)
if m: chat = m.group(1).strip()
except Exception:
pass
return token, chat
def _push_telegram(account, text):
"""v2.89.49 — 마크다운 모드는 *,[,(,),# 같은 특수문자 많은 보고서에서 자주 400 거부.
이전엔 그래도 'sent' print해서 사용자한테 가짜 성공 보고. 이제 plain text 모드로
안전하게 보내고 HTTP status 체크해서 진짜 성공/실패 정확히 알려줌."""
token, chat = _resolve_telegram(account)
if not token or not chat:
print("⚠️ 텔레그램 토큰/chat_id 미설정 — 전송 안 함", file=sys.stderr)
return
try:
import requests
# plain text (parse_mode 없음) — 어떤 특수문자든 통과
r = requests.post(
f"https://api.telegram.org/bot{token}/sendMessage",
json={"chat_id": chat, "text": text[:4000]},
timeout=10,
)
if r.status_code == 200:
print("📨 텔레그램 전송 성공", file=sys.stderr)
else:
try:
err = r.json().get("description", r.text[:200])
except Exception:
err = r.text[:200]
print(f"⚠️ 텔레그램 전송 실패 (HTTP {r.status_code}): {err}", file=sys.stderr)
except Exception as e:
print(f"⚠️ 텔레그램 전송 에러: {e}", file=sys.stderr)
def _fmt_num(n):
if n >= 1_000_000: return f"{n/1_000_000:.1f}M"
if n >= 1_000: return f"{n/1_000:.1f}K"
return f"{n:,}"
def _parse_duration(iso):
"""ISO 8601 duration (PT5M30S) → seconds"""
m = re.match(r'PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?', iso or '')
if not m: return 0
h, mn, s = (int(x) if x else 0 for x in m.groups())
return h * 3600 + mn * 60 + s
def _fmt_duration(secs):
if secs >= 3600: return f"{secs//3600}시간 {(secs%3600)//60}"
if secs >= 60: return f"{secs//60}{secs%60}"
return f"{secs}"
def _korean_weekday(dt):
return ["","","","","","",""][dt.weekday()]
def main():
if not os.path.exists(ACCOUNT):
print("❌ youtube_account.json이 없어요. 직원 에이전트 보기 → YouTube → 도구 ⚙️에서 API 키와 채널 ID를 입력하세요.")
sys.exit(1)
acct = _load(ACCOUNT)
cfg = _load(CONFIG) if os.path.exists(CONFIG) else {}
api_key = (acct.get("YOUTUBE_API_KEY") or "").strip()
handle = (acct.get("MY_CHANNEL_HANDLE") or "").strip()
chan_id = (acct.get("MY_CHANNEL_ID") or "").strip()
if not api_key:
print("❌ YOUTUBE_API_KEY 미설정. youtube_account.json에 채워주세요.")
sys.exit(1)
if not (handle or chan_id):
print("❌ MY_CHANNEL_HANDLE 또는 MY_CHANNEL_ID 필요.")
sys.exit(1)
lookback = int(cfg.get("LOOKBACK_DAYS", 30))
top_n = int(cfg.get("TOP_N", 15))
comment_samples = int(cfg.get("COMMENT_SAMPLES", 5))
try:
from googleapiclient.discovery import build
except ImportError:
print("❌ google-api-python-client 미설치. pip install google-api-python-client requests")
sys.exit(1)
youtube = build("youtube", "v3", developerKey=api_key)
cid = _resolve_channel_id(youtube, handle, chan_id)
if not cid:
print("❌ 채널 ID를 찾지 못했어요. youtube_account.json의 핸들/ID 확인.")
sys.exit(1)
# === 1. 채널 메타 ===
print(f"🔍 채널 정보 가져오는 중...", file=sys.stderr)
cr = youtube.channels().list(part="snippet,statistics,contentDetails,brandingSettings", id=cid).execute()
cit = cr.get("items", [])
if not cit:
print(f"❌ 채널 데이터 없음 (ID: {cid})")
sys.exit(1)
ch = cit[0]
snip = ch.get("snippet", {})
cstats = ch.get("statistics", {})
# v2.89.55 — YouTube API가 가끔 &amp; / &#39; 같은 HTML entity로 인코딩된 제목 반환.
# 이걸 그대로 출력하면 채팅창에서 "&#39;" 가 literal로 보임. 미리 디코드.
ch_title = html_lib.unescape(snip.get("title", "") or "")
custom_url = snip.get("customUrl", "")
published = (snip.get("publishedAt", "") or "")[:10]
country = snip.get("country", "")
sub_count = int(cstats.get("subscriberCount", 0))
subs_hidden = cstats.get("hiddenSubscriberCount", False)
view_count_total = int(cstats.get("viewCount", 0))
video_count_total = int(cstats.get("videoCount", 0))
if published:
try:
age_days = (datetime.date.today() - datetime.date.fromisoformat(published)).days
except Exception:
age_days = 0
else:
age_days = 0
age_years = age_days / 365.25 if age_days > 0 else 0
avg_views_per_video_alltime = view_count_total // video_count_total if video_count_total else 0
# === 2. 최근 영상 목록 ===
print(f"🔍 최근 {lookback}일 영상 가져오는 중...", file=sys.stderr)
after = (datetime.datetime.utcnow() - datetime.timedelta(days=lookback)).isoformat("T") + "Z"
sr = youtube.search().list(part="snippet", channelId=cid, maxResults=top_n,
order="date", publishedAfter=after, type="video").execute()
vids = [(it["id"]["videoId"], it["snippet"]["title"], it["snippet"]["publishedAt"])
for it in sr.get("items", [])]
if not vids:
# Fallback to most recent regardless of lookback window
sr = youtube.search().list(part="snippet", channelId=cid, maxResults=top_n,
order="date", type="video").execute()
vids = [(it["id"]["videoId"], it["snippet"]["title"], it["snippet"]["publishedAt"])
for it in sr.get("items", [])]
if not vids:
# v2.89.55 — 빈 영상 시 stderr로. stdout이 비어 있어야 TS shortcut이 실패로 정확히 처리.
print(f"⚠️ 업로드된 영상이 없어요.", file=sys.stderr)
sys.exit(0)
# === 3. 영상 상세 통계 ===
print(f"🔍 영상 {len(vids)}개 상세 통계 + 길이·태그 가져오는 중...", file=sys.stderr)
vstats = youtube.videos().list(
part="statistics,contentDetails,snippet",
id=",".join(v[0] for v in vids)
).execute()
sm = {it["id"]: it for it in vstats.get("items", [])}
rows = []
for vid, vtitle, pub in vids:
item = sm.get(vid, {})
s = item.get("statistics", {})
cd = item.get("contentDetails", {})
sn = item.get("snippet", {})
views = int(s.get("viewCount", 0))
likes = int(s.get("likeCount", 0))
comments = int(s.get("commentCount", 0))
dur_sec = _parse_duration(cd.get("duration", "PT0S"))
like_rate = (likes / views * 100) if views > 0 else 0
comment_rate = (comments / views * 100) if views > 0 else 0
try:
pub_dt = datetime.datetime.fromisoformat(pub.replace("Z", "+00:00"))
weekday = _korean_weekday(pub_dt)
hour = pub_dt.hour
except Exception:
weekday, hour = "-", 0
rows.append({
# v2.89.55 — title HTML entity 디코드 (&#39; → ', &amp; → & 등)
"id": vid, "title": html_lib.unescape(vtitle or ""), "pub": pub[:10],
"weekday": weekday, "hour": hour,
"views": views, "likes": likes, "comments": comments,
"duration_sec": dur_sec,
"like_rate": like_rate, "comment_rate": comment_rate,
"tags": sn.get("tags", []) or [],
"is_short": dur_sec <= 60,
})
# === 4. 집계 ===
views_list = [r["views"] for r in rows]
median_views = int(statistics.median(views_list)) if views_list else 0
avg_views = int(statistics.mean(views_list)) if views_list else 0
avg_likes = int(statistics.mean([r["likes"] for r in rows])) if rows else 0
avg_comments = int(statistics.mean([r["comments"] for r in rows])) if rows else 0
avg_duration = int(statistics.mean([r["duration_sec"] for r in rows])) if rows else 0
avg_like_rate = statistics.mean([r["like_rate"] for r in rows]) if rows else 0
avg_comment_rate = statistics.mean([r["comment_rate"] for r in rows]) if rows else 0
title_lengths = [len(r["title"]) for r in rows]
avg_title_len = int(statistics.mean(title_lengths)) if title_lengths else 0
shorts_count = sum(1 for r in rows if r["is_short"])
rows_sorted = sorted(rows, key=lambda r: r["views"], reverse=True)
top_videos = rows_sorted[:3]
bottom_videos = rows_sorted[-3:][::-1] if len(rows_sorted) >= 4 else []
# 요일·시간대 패턴
weekday_views = {}
for r in rows:
weekday_views.setdefault(r["weekday"], []).append(r["views"])
weekday_avg = {wd: int(statistics.mean(vs)) for wd, vs in weekday_views.items()}
# 상위 영상 제목 키워드
top_title_words = Counter()
stop_kr = {'그리고','근데','너무','진짜','정말','내가','지금','이거','저는','제가','우리'}
stop_en = {'this','that','and','the','for','with','have','will','your','from','about'}
for r in top_videos:
words = re.findall(r'[가-힣]+|[a-zA-Z]+', r["title"])
top_title_words.update(w for w in words if len(w) >= 2 and w.lower() not in stop_en and w not in stop_kr)
top_keywords = [w for w, _ in top_title_words.most_common(8)]
# === 5. 인기 댓글 샘플 (상위 3개 영상) ===
print(f"💬 상위 영상의 인기 댓글 가져오는 중...", file=sys.stderr)
comments_by_video = {}
for r in top_videos[:3]:
try:
cr_resp = youtube.commentThreads().list(
part="snippet", videoId=r["id"], maxResults=comment_samples, order="relevance"
).execute()
comments_by_video[r["id"]] = [
{
# v2.89.55 — author/text도 HTML entity 디코드
"author": html_lib.unescape(c["snippet"]["topLevelComment"]["snippet"].get("authorDisplayName", "") or ""),
"text": html_lib.unescape(c["snippet"]["topLevelComment"]["snippet"].get("textOriginal", "") or "")[:200],
"likes": int(c["snippet"]["topLevelComment"]["snippet"].get("likeCount", 0)),
}
for c in cr_resp.get("items", [])
]
except Exception:
comments_by_video[r["id"]] = [] # 댓글 비활성 영상이면 403
# === 6. 종합 보고서 ===
# v2.89.50 — 시각적으로 더 멋진 레이아웃. 블록인용·이모지 평가·시각 분리선 활용.
sub_str = "비공개" if subs_hidden else f"{_fmt_num(sub_count)}"
like_rating = "🟢 좋음" if avg_like_rate >= 2.0 else ("🟡 보통" if avg_like_rate >= 1.0 else "🔴 개선")
comment_rating = "🟢 좋음" if avg_comment_rate >= 0.5 else ("🟡 보통" if avg_comment_rate >= 0.2 else "🔴 개선")
L = []
L.append(f"# 🎬 {ch_title}")
L.append(f"_{time.strftime('%Y-%m-%d %H:%M')} · 최근 {lookback}일 분석 · 영상 {len(rows)}개_")
L.append("")
# 채널 메타 — 인용 블록으로 한눈에
L.append(f"> **{sub_str}** 구독자 · **{_fmt_num(view_count_total)}** 누적 조회 · **{video_count_total:,}개** 영상" + (f" · **{age_years:.1f}년** 운영" if age_years > 0 else ""))
L.append(f"> 핸들 `{custom_url or handle or '-'}`" + (f" · 🌍 {country}" if country else "") + f" · 영상당 평균 **{_fmt_num(avg_views_per_video_alltime)}** 조회")
L.append("")
L.append("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
L.append("")
# 최근 성과 요약 — 카드 스타일
L.append(f"## 📊 최근 {lookback}일 성과 한눈에")
L.append("")
L.append("| 지표 | 값 | 평가 |")
L.append("|---|---|---|")
pace = (len(rows) * 30 / lookback) if lookback > 0 else 0
pace_rating = "🟢 활발" if pace >= 4 else ("🟡 보통" if pace >= 2 else "🔴 저조")
L.append(f"| 업로드 | {len(rows)}개 (월 {pace:.1f}개) | {pace_rating} |")
if rows:
L.append(f"| 조회수 중간값 | **{_fmt_num(median_views)}** | 최고 {_fmt_num(rows_sorted[0]['views'])} · 최저 {_fmt_num(rows_sorted[-1]['views'])} |")
L.append(f"| 좋아요율 | **{avg_like_rate:.2f}%** | {like_rating} (업계 2~5%) |")
L.append(f"| 댓글율 | **{avg_comment_rate:.2f}%** | {comment_rating} (업계 0.3~1%) |")
L.append(f"| 평균 길이 | {_fmt_duration(avg_duration)} | 제목 평균 {avg_title_len}자 |")
if shorts_count:
L.append(f"| Shorts | {shorts_count}개 / {len(rows)} | - |")
L.append("")
L.append("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
L.append("")
# 영상별 상세 표
L.append("## 📺 영상별 상세 (조회수 순)")
L.append("| # | 조회수 | 좋아요 (율) | 댓글 (율) | 길이 | 발행 | 제목 |")
L.append("|---|---|---|---|---|---|---|")
for i, r in enumerate(rows_sorted, 1):
marker = "🔥" if r["views"] >= median_views * 1.5 else ("👍" if r["views"] >= median_views else "🥶")
title_short = r['title'].replace('|', '\\|')[:60]
L.append(f"| {i}{marker} | {_fmt_num(r['views'])} | {_fmt_num(r['likes'])} ({r['like_rate']:.1f}%) | {_fmt_num(r['comments'])} ({r['comment_rate']:.1f}%) | {_fmt_duration(r['duration_sec'])} | {r['pub']}({r['weekday']}) | {title_short} |")
L.append("")
# 상위 영상 심층 분석 — 카드 스타일 + 메달 이모지
L.append("## 🏆 TOP 3 — 무엇이 잘 됐나")
L.append("")
medals = ["🥇", "🥈", "🥉"]
for idx, r in enumerate(top_videos):
medal = medals[idx] if idx < 3 else "👍"
L.append(f"### {medal} {_fmt_num(r['views'])}회 · {r['title']}")
L.append("")
L.append(f"> 📅 {r['pub']} ({r['weekday']}요일 {r['hour']:02d}시) · ⏱ {_fmt_duration(r['duration_sec'])} · 👍 {r['like_rate']:.2f}% · 💬 {r['comment_rate']:.2f}%")
if r['tags']:
tag_str = ' '.join(f"`{t}`" for t in r['tags'][:5])
L.append(f"> 🏷 {tag_str}" + ('' if len(r['tags']) > 5 else ''))
L.append(f"> 🔗 [영상 보기](https://youtu.be/{r['id']}) · 🖼 [썸네일](https://i.ytimg.com/vi/{r['id']}/mqdefault.jpg)")
cs = comments_by_video.get(r["id"], [])
if cs:
L.append("")
L.append("**💬 인기 댓글:**")
for c in cs[:3]:
txt = c['text'].replace(chr(10), ' ').replace(chr(13), ' ')[:140]
L.append(f"> _{c['author']}_ (👍{c['likes']}): {txt}")
L.append("")
# 하위 영상 — 시각적으로 부진 강조
if bottom_videos:
L.append("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
L.append("")
L.append("## 🥶 하위 영상 — 개선 필요")
L.append("")
for r in bottom_videos:
gap_pct = int((1 - r['views'] / median_views) * 100) if median_views else 0
L.append(f"- **{_fmt_num(r['views'])}회** · 중간값 대비 **-{gap_pct}%** ↓")
L.append(f" - {r['title']}")
L.append(f" - 📅 {r['pub']}({r['weekday']}, {r['hour']:02d}시) · ⏱ {_fmt_duration(r['duration_sec'])} · 🔗 [영상](https://youtu.be/{r['id']})")
L.append("")
# 패턴
L.append("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
L.append("")
L.append("## 🔍 패턴 분석")
L.append("")
if weekday_avg and len(weekday_avg) >= 2:
best_day = max(weekday_avg.items(), key=lambda x: x[1])
worst_day = min(weekday_avg.items(), key=lambda x: x[1])
ratio = best_day[1] / worst_day[1] if worst_day[1] else 1
L.append(f"- 📅 **최고 요일**: {best_day[0]}요일 (평균 {_fmt_num(best_day[1])}회) — 최저 대비 **{ratio:.1f}배**")
L.append(f"- 📅 **최저 요일**: {worst_day[0]}요일 (평균 {_fmt_num(worst_day[1])}회)")
if top_keywords:
L.append(f"- 🔑 **상위 영상 키워드**: {' '.join('`'+k+'`' for k in top_keywords)}")
if title_lengths:
L.append(f"- 📝 **제목 길이**: 평균 {avg_title_len}자 (최단 {min(title_lengths)}자 · 최장 {max(title_lengths)}자)")
if avg_duration > 0:
L.append(f"- ⏱ **영상 길이**: 평균 {_fmt_duration(avg_duration)}" + (f" · Shorts(60초 이하) {shorts_count}/{len(rows)}" if shorts_count else ""))
L.append("")
# 액션 추천 — 카드 스타일
L.append("")
L.append("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
L.append("")
L.append("## 🎯 다음 액션 (우선순위)")
L.append("")
recs = []
if bottom_videos:
worst = bottom_videos[0]
recs.append(("🔴", f"**부진 영상 살리기** — `{worst['title'][:40]}` ({_fmt_num(worst['views'])}회). 썸네일 A/B 또는 제목 리네이밍."))
if top_videos:
winner = top_videos[0]
recs.append(("🔥", f"**떡상 패턴 복제** — `{winner['title'][:40]}` ({_fmt_num(winner['views'])}회). 같은 후크/포맷으로 후속편."))
if weekday_avg and len(weekday_avg) >= 3:
best_day = max(weekday_avg.items(), key=lambda x: x[1])[0]
recs.append(("📅", f"**발행 요일 최적화** — {best_day}요일 영상이 평균 가장 잘 됨. 다음 업로드 {best_day}요일 추천."))
if avg_like_rate < 2.0 and avg_views > 100:
recs.append(("👍", f"**좋아요율 개선** — 현재 {avg_like_rate:.2f}% (업계 2~5%). 영상 끝 콜아웃 강화."))
if avg_comment_rate < 0.3 and avg_views > 100:
recs.append(("💬", f"**댓글 유도 강화** — 현재 {avg_comment_rate:.2f}% (업계 0.3~1%). 영상 중간 시청자 의견 질문 삽입."))
if top_keywords:
recs.append(("🔑", f"**제목 키워드 활용** — 상위 영상의 `{', '.join(top_keywords[:3])}` 키워드를 다음 제목에 통합."))
if shorts_count == 0 and len(rows) >= 5:
recs.append(("📱", f"**Shorts 시도** — 최근 {lookback}일에 Shorts 0개. 신규 유입 채널로 좋음."))
if pace < 2:
recs.append(("", f"**업로드 빈도 점검** — 월 {pace:.1f}개 페이스. 알고리즘 친화적 페이스는 주 1회+."))
if not recs:
recs.append(("", "데이터 부족 — 더 많은 영상 업로드 후 재분석 권장"))
for i, (icon, rec) in enumerate(recs, 1):
L.append(f"**{i}. {icon} {rec}**" if i == 1 else f"{i}. {icon} {rec}")
L.append("")
# 시청자 반응 키워드 (상위 영상 댓글 기반)
all_comments = []
for cs in comments_by_video.values():
all_comments.extend(c["text"] for c in cs)
if all_comments:
L.append("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
L.append("")
L.append("## 💬 시청자가 남긴 키워드")
L.append("")
all_text = " ".join(all_comments)
words = re.findall(r'[가-힣]{2,}|[a-zA-Z]{3,}', all_text)
# URL 조각·도메인은 의미 없으니 제외
url_noise = {'https', 'http', 'youtu', 'www', 'com'}
words = [w for w in words if w.lower() not in stop_en and w not in stop_kr and w.lower() not in url_noise and not re.match(r'^[a-zA-Z0-9_]{8,}$', w)]
word_freq = Counter(words).most_common(8)
if word_freq:
kw_line = ' · '.join(f"`{w}`({c})" for w, c in word_freq)
L.append(kw_line)
L.append("")
L.append("> 시청자 머릿속에 남은 단어. 다음 영상 제목·썸네일·후크에 활용.")
L.append("")
summary = chr(10).join(L)
# v2.89.49 — stdout은 보고서 markdown만. 메타·진단 메시지는 stderr로.
print(summary)
with open(REPORT, "a", encoding="utf-8") as f:
f.write(chr(10) + chr(10) + summary + chr(10) + chr(10) + "---" + chr(10))
print(f"\n✅ 보고서 저장: {REPORT}", file=sys.stderr)
# Telegram (4096자 제한 — plain text라 마크다운 특수문자 그대로 보내도 통과)
tg_lines = []
tg_lines.append(f"📊 {ch_title} — 채널 분석")
tg_lines.append(f"({time.strftime('%Y-%m-%d %H:%M')} · 최근 {lookback}일 · 영상 {len(rows)}개)")
tg_lines.append("")
tg_lines.append(f"구독자 {sub_str} · 누적 {_fmt_num(view_count_total)} · 총 {video_count_total}")
if rows:
tg_lines.append(f"중간값 {_fmt_num(median_views)}회 · 최고 {_fmt_num(rows_sorted[0]['views'])} · 최저 {_fmt_num(rows_sorted[-1]['views'])}")
tg_lines.append(f"좋아요율 {avg_like_rate:.2f}% · 댓글율 {avg_comment_rate:.2f}%")
tg_lines.append("")
if top_videos:
tg_lines.append(f"🏆 최고: {_fmt_num(top_videos[0]['views'])} {top_videos[0]['title'][:40]}")
if bottom_videos:
tg_lines.append(f"🥶 부진: {_fmt_num(bottom_videos[0]['views'])} {bottom_videos[0]['title'][:40]}")
tg_lines.append("")
if recs:
tg_lines.append("🎯 액션:")
for i, (icon, rec) in enumerate(recs[:3], 1):
# 마크다운 ** 제거하고 plain text로
clean = re.sub(r'\*\*|`', '', rec.split('')[0] if '' in rec else rec)
tg_lines.append(f"{i}. {icon} {clean[:80]}")
tg_lines.append("")
tg_lines.append("(전체 분석은 IDE 채팅창 확인)")
tg_text = chr(10).join(tg_lines)
_push_telegram(acct, tg_text)
if __name__ == "__main__":
main()
@@ -0,0 +1,39 @@
# 📨 텔레그램 보고
다른 도구가 보고를 메신저로 보낼 때 호출하는 통신선. ▶ 실행하면 **연결 테스트** — 받으면 OK, 안 오면 토큰/chat_id 다시 확인.
## 토큰은 어디에 넣나요? — **Secretary 비서가 정답**
회사 아키텍처상 비서(Secretary) 에이전트가 메신저 담당이에요. 거기 한 번만 넣으면 모든 에이전트가 공유합니다:
```
_agents/secretary/config.md
```
이 파일에 다음 두 줄:
```
- TELEGRAM_BOT_TOKEN: <토큰>
- TELEGRAM_CHAT_ID: <chat_id>
```
(이 파일은 `.gitignore`에 의해 git에 안 올라갑니다.)
### 구버전 호환 (선택)
이전 버전에서 `youtube_account.json`에 텔레그램 입력하셨다면 그것도 fallback으로 동작합니다 — 다만 비서 쪽이 우선이고 캐노니컬이에요.
## 어떻게 도와주나요?
- ✅ 연결 확인 핑 (인자 없이 실행)
- 📨 모든 에이전트(YouTube, Secretary 등)가 자동 보고 보내는 채널
- 🔕 토큰/chat_id 미설정이면 다른 도구는 텔레그램 단계만 건너뜁니다
## 봇 만드는 법 (한 번만)
1. 텔레그램 [@BotFather](https://t.me/BotFather) → `/newbot` → 토큰 받음
2. 봇에게 `/start` 등 메시지 1회 보내기
3. `https://api.telegram.org/bot<TOKEN>/getUpdates` 열어 `chat.id` 확인
4. `_agents/secretary/config.md``TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`에 입력
5. 이 도구 [▶ 실행] → 핑 메시지 도착하면 완료
## 다른 도구에서 어떻게 쓰이나?
- "내 영상 체크" → 떡상/부진 요약 푸시
- "경쟁 채널 분석" → 다음 액션 브리프 푸시
- 비서의 전사 데일리 브리핑도 같은 라인 사용
@@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""Telegram Notify — small wrapper that sends a message to your Telegram bot.
Two modes:
1. No CLI arg → sends a connectivity test ("✅ 텔레그램 연결 정상").
2. With CLI arg(s) → sends those as the message body. Other tools can call
this script to push their summaries.
telegram_v3 — Secretary's tools/telegram_setup.json is the canonical
UI-managed home (input via Skills ⚙️). Falls back to legacy config.md
and finally to youtube_account.json so older setups keep working."""
import os, json, sys, time, re
HERE = os.path.dirname(os.path.abspath(__file__))
ACCOUNT = os.path.join(HERE, "youtube_account.json")
# tools/ → youtube/ → _agents/ → brain root
BRAIN_ROOT = os.path.abspath(os.path.join(HERE, "..", "..", ".."))
SECRETARY_TOOL_JSON = os.path.join(BRAIN_ROOT, "_agents", "secretary", "tools", "telegram_setup.json")
SECRETARY_CFG = os.path.join(BRAIN_ROOT, "_agents", "secretary", "config.md")
def _resolve_telegram():
"""Secretary tool JSON > Secretary legacy md > youtube_account.json."""
token, chat = "", ""
if os.path.exists(SECRETARY_TOOL_JSON):
try:
with open(SECRETARY_TOOL_JSON, "r", encoding="utf-8") as f:
cfg = json.load(f)
token = (cfg.get("TELEGRAM_BOT_TOKEN") or "").strip()
chat = (cfg.get("TELEGRAM_CHAT_ID") or "").strip()
except Exception:
pass
if (not token or not chat) and os.path.exists(SECRETARY_CFG):
try:
with open(SECRETARY_CFG, "r", encoding="utf-8") as f:
txt = f.read()
if not token:
m = re.search(r"TELEGRAM_BOT_TOKEN\s*[:=]\s*([A-Za-z0-9:_\-]+)", txt)
if m: token = m.group(1).strip()
if not chat:
m = re.search(r"TELEGRAM_CHAT_ID\s*[:=]\s*(-?\d+)", txt)
if m: chat = m.group(1).strip()
except Exception:
pass
if (not token or not chat) and os.path.exists(ACCOUNT):
try:
with open(ACCOUNT, "r", encoding="utf-8") as f:
acct = json.load(f)
if not token: token = (acct.get("TELEGRAM_BOT_TOKEN") or "").strip()
if not chat: chat = (acct.get("TELEGRAM_CHAT_ID") or "").strip()
except Exception:
pass
return token, chat
def main():
token, chat = _resolve_telegram()
if not token or not chat:
print("❌ TELEGRAM_BOT_TOKEN 또는 TELEGRAM_CHAT_ID를 못 찾았어요.")
print(" 권장: 비서(Secretary) 클릭 → Skills → 📨 텔레그램 연결 ⚙️ → 폼에 입력")
print(" 봇 만들기: Telegram → @BotFather → /newbot")
print(" chat_id: 봇에 메시지 1회 → https://api.telegram.org/bot<TOKEN>/getUpdates 에서 chat.id 확인")
sys.exit(1)
if len(sys.argv) > 1:
body = " ".join(sys.argv[1:])
else:
body = f"✅ 텔레그램 연결 정상 — {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n비서(Secretary) 또는 YouTube 도구가 이 채널로 보고를 보낼 수 있습니다."
try:
import requests
except ImportError:
print("❌ pip install requests")
sys.exit(1)
try:
r = requests.post(
f"https://api.telegram.org/bot{token}/sendMessage",
json={"chat_id": chat, "text": body, "parse_mode": "Markdown"},
timeout=15,
)
r.raise_for_status()
print(f"✅ 전송 OK ({len(body)}자)")
except Exception as e:
print(f"❌ 전송 실패: {e}")
if "Bad Request" in str(e):
print(" chat_id가 정확한지, 봇과 한 번이라도 대화를 시작했는지 확인하세요.")
sys.exit(1)
if __name__ == "__main__":
main()
@@ -0,0 +1,21 @@
# 🎯 트렌드 스나이퍼
유튜브 Data API로 최근 30일 떡상 영상을 수집하고, 로컬 LLM(Ollama/LM Studio)으로 패턴을 분석해 다음 영상 기획안(제목·썸네일·후크)을 도출합니다.
## 필요한 것
- Python 3 + `pip install google-api-python-client requests`
- `youtube_account.json``YOUTUBE_API_KEY` 채우기 (한 번만)
- 로컬 LLM (Ollama 또는 LM Studio)이 켜져 있어야 함
## 설정값 (trend_sniper.json)
- `TARGET_KEYWORDS` — 분석할 키워드 배열
- (API 키·Ollama URL·모델은 공유 `youtube_account.json`에서 자동 로드)
## 실행 방법
패널의 [▶ 실행] 버튼을 누르거나 터미널에서:
```bash
python trend_sniper.py
```
## 출력
같은 폴더에 `trend_sniper_report.md` 누적 저장.
@@ -0,0 +1,151 @@
#!/usr/bin/env python3
"""Trend Sniper — pulls top YouTube videos for target keywords, asks a local
LLM (Ollama/LM Studio) to extract the algorithmic patterns, and writes a
planning report next to this script.
Shared keys (API key, OLLAMA_URL, MODEL) come from youtube_account.json so
you only set them once. Per-tool keys (TARGET_KEYWORDS) come from
trend_sniper.json. If a key exists in both, trend_sniper.json wins.
Requires: pip install google-api-python-client requests
"""
import os, json, time, random, datetime, sys
HERE = os.path.dirname(os.path.abspath(__file__))
CONFIG_PATH = os.path.join(HERE, "trend_sniper.json")
ACCOUNT_PATH = os.path.join(HERE, "youtube_account.json")
REPORT_PATH = os.path.join(HERE, "trend_sniper_report.md")
def load_config():
try:
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
print(f"❌ 설정 파일을 읽을 수 없어요: {CONFIG_PATH}\n{e}")
sys.exit(1)
def load_account():
try:
if os.path.exists(ACCOUNT_PATH):
with open(ACCOUNT_PATH, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
pass
return {}
def _shared(cfg, acct, key, default=""):
"""Per-tool config wins; falls back to shared account; finally default."""
v = cfg.get(key)
if v not in (None, "", []):
return v
v = acct.get(key)
if v not in (None, "", []):
return v
return default
def main():
cfg = load_config()
acct = load_account()
api_key = (_shared(cfg, acct, "YOUTUBE_API_KEY") or "").strip()
if not api_key:
print("⚠️ YOUTUBE_API_KEY가 비어있어요. youtube_account.json 또는 trend_sniper.json에 입력하세요.")
print(" 발급: https://console.cloud.google.com/ → YouTube Data API v3 사용 설정 → 사용자 인증 정보 → API 키")
sys.exit(1)
target_keywords = cfg.get("TARGET_KEYWORDS", [])
if not target_keywords:
print("⚠️ TARGET_KEYWORDS가 비어있어요. 분석할 키워드를 1개 이상 추가하세요.")
sys.exit(1)
ollama_url = (_shared(cfg, acct, "OLLAMA_URL", "http://127.0.0.1:11434") or "http://127.0.0.1:11434").rstrip("/")
model = _shared(cfg, acct, "MODEL", "") or ""
pick = min(2, len(target_keywords))
chosen = random.sample(target_keywords, pick)
try:
from googleapiclient.discovery import build
except ImportError:
print("❌ google-api-python-client가 설치되지 않았어요.")
print(" 설치: pip install google-api-python-client requests")
sys.exit(1)
try:
import requests
except ImportError:
print("❌ requests가 설치되지 않았어요. pip install requests")
sys.exit(1)
print(f"\n🎯 [트렌드 스나이퍼] 키워드 {chosen} 스캔 시작...")
youtube = build('youtube', 'v3', developerKey=api_key)
last_month = (datetime.datetime.utcnow() - datetime.timedelta(days=30)).isoformat("T") + "Z"
sniper_data = []
for q in chosen:
print(f"📡 [{q}] 검색 중...")
try:
req = youtube.search().list(
part="snippet", q=q, maxResults=5, order="viewCount",
publishedAfter=last_month, type="video"
)
res = req.execute()
for item in res.get('items', []):
title = item['snippet']['title']
channel = item['snippet']['channelTitle']
sniper_data.append(f"[{q}] 채널: {channel} | 제목: {title}")
except Exception as e:
print(f"❌ 검색 오류 ({q}): {e}")
if not sniper_data:
print("❌ 수집된 데이터 없음. API 키 한도/네트워크 확인.")
sys.exit(1)
data_text = "\n".join(sniper_data)
prompt = f"""당신은 유튜브 알고리즘 마스터마인드입니다. 아래는 최근 30일 떡상 영상입니다.
[키워드] {', '.join(chosen)}
[데이터]
{data_text}
분석해서 마크다운 보고서를 작성하세요. 반드시 3섹션:
1. 🌍 트렌드 해킹 분석 — 어떤 패턴이 조회수를 끌고 있는지
2. 🎯 빈집 털기 전략 — 차별화 가능한 틈새 주제
3. 🎬 파괴적 영상 기획안 — 썸네일 카피, 제목 3개, 후킹 오프닝(첫 5초)
"""
print("🧠 [LLM 분석 중...]")
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 not models:
print("❌ 로컬 LLM에 설치된 모델이 없어요. Ollama/LM Studio에서 모델을 풀(pull)하세요.")
sys.exit(1)
model = models[0]
except Exception as e:
print(f"❌ 로컬 LLM 연결 실패 ({ollama_url}): {e}")
sys.exit(1)
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()
except Exception as e:
print(f"❌ LLM 호출 실패: {e}")
sys.exit(1)
print("\n" + "="*60)
print(report)
print("="*60)
with open(REPORT_PATH, "a", encoding="utf-8") as f:
now = time.strftime('%Y-%m-%d %H:%M:%S')
f.write(f"\n\n# 🎯 트렌드 스나이핑 보고서 — {now}\n")
f.write(f"## 📡 키워드: {', '.join(chosen)}\n\n")
f.write(report)
f.write("\n\n---\n")
print(f"\n✅ 보고서 저장: {REPORT_PATH}")
if __name__ == "__main__":
main()
@@ -0,0 +1,26 @@
# 🔑 계정 / 채널 (공유 설정)
여기 한 번만 채워두면 다른 모든 YouTube 도구(트렌드 스나이퍼·내 영상 체크·댓글 수집기·경쟁 채널 분석·텔레그램 보고)가 이 값을 그대로 가져다 씁니다. 매번 도구마다 같은 키를 넣지 않아도 돼요.
## 채워야 하는 항목
| 키 | 설명 | 채우는 법 |
|---|---|---|
| `YOUTUBE_API_KEY` | YouTube Data API v3 키 | [console.cloud.google.com](https://console.cloud.google.com/) → 프로젝트 → "YouTube Data API v3" 사용 설정 → 사용자 인증 정보 → API 키. 무료 한도 충분(하루 10,000 단위). |
| `MY_CHANNEL_HANDLE` | 본인 채널 @핸들 | 예: `@mychannel`. 핸들 또는 ID 둘 중 하나만 채우면 됨. |
| `MY_CHANNEL_ID` | 본인 채널 ID (UCxxxx) | 핸들로 못 잡힐 때 백업용. studio.youtube.com → 설정 → 채널에서 확인. |
| `WATCHED_CHANNELS` | 댓글 수집 대상 채널 핸들 목록 | 예: `["@channel_a", "@channel_b"]`. 댓글 수집기가 이 채널들 최근 영상의 댓글을 메모리로 가져옵니다. |
| `COMPETITOR_CHANNELS` | 경쟁 채널 분석 대상 | 같은 형식. 경쟁 채널 분석 도구가 패턴을 뽑아 다음 액션을 추천합니다. |
| `TELEGRAM_BOT_TOKEN` | (선택) 봇 토큰 | **권장: 비서(Secretary) 에이전트의 `_agents/secretary/config.md`에 입력하세요.** 거기 넣으면 모든 에이전트가 공유. 여기 입력해도 동작은 하지만 fallback일 뿐. |
| `TELEGRAM_CHAT_ID` | (선택) chat_id | 위와 같음 — Secretary가 우선. |
| `OLLAMA_URL` | 로컬 LLM 주소 | 기본 `http://127.0.0.1:11434`. LM Studio면 보통 `http://127.0.0.1:1234`. |
| `MODEL` | 분석에 쓸 모델 이름 | 비워두면 첫 번째로 발견된 모델을 자동 선택. |
## 실행하면?
입력값이 제대로 들어왔는지 확인 리포트만 출력합니다 (실제 데이터 호출 X). 키가 비어있으면 알려줍니다.
## 텔레그램은 따로 — 비서(Secretary)에 입력
텔레그램 토큰은 비서 담당이에요. `_agents/secretary/config.md``TELEGRAM_BOT_TOKEN: <토큰>` 한 줄 + `TELEGRAM_CHAT_ID: <id>` 한 줄 넣으면 **모든 에이전트가 공유**합니다 (YouTube 도구 포함). 여기 youtube_account.json에 같이 넣어도 동작하지만 비서 쪽이 우선이에요.
## 어디 저장되나?
`youtube_account.json``.gitignore`에 의해 git에 안 올라갑니다 (API 키·토큰 보호).
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""YouTube Account / Channels — shared config for every YouTube tool.
This script doesn't fetch anything by itself. It's listed in the agent panel
so you can click once and fill in your API key, channel, watched
channels, etc. and every other tool will read from here.
Running it just prints a sanity-check report so you can confirm the values
are loaded correctly (without leaking the full API key)."""
import os, json, sys
HERE = os.path.dirname(os.path.abspath(__file__))
CONFIG_PATH = os.path.join(HERE, "youtube_account.json")
def load():
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
return json.load(f)
def main():
cfg = load()
api = (cfg.get("YOUTUBE_API_KEY") or "").strip()
masked = (api[:4] + "" + api[-3:]) if len(api) >= 8 else ("(빈 값)" if not api else "(짧음)")
print("─── YouTube 계정 / 채널 설정 ───")
print(f" API 키 : {masked}")
print(f" 내 채널 핸들 : {cfg.get('MY_CHANNEL_HANDLE') or '(없음)'}")
print(f" 내 채널 ID : {cfg.get('MY_CHANNEL_ID') or '(없음)'}")
watched = cfg.get('WATCHED_CHANNELS') or []
print(f" 감시 채널 ({len(watched)}개) : {', '.join(watched) if watched else '(없음)'}")
competitors = cfg.get('COMPETITOR_CHANNELS') or []
print(f" 경쟁 채널 ({len(competitors)}개): {', '.join(competitors) if competitors else '(없음)'}")
tg_bot = (cfg.get('TELEGRAM_BOT_TOKEN') or '').strip()
tg_chat = (cfg.get('TELEGRAM_CHAT_ID') or '').strip()
if tg_bot and tg_chat:
print(f" 텔레그램 : 연결됨 (chat {tg_chat})")
else:
print(f" 텔레그램 : 미설정 (보고 알림 비활성)")
print(f" Ollama URL : {cfg.get('OLLAMA_URL') or 'http://127.0.0.1:11434'}")
print(f" 분석 모델 : {cfg.get('MODEL') or '(자동 선택)'}")
if not api:
print("\n⚠️ API 키가 비어있어요. 다른 도구들이 동작하지 않습니다.")
print(" 발급: https://console.cloud.google.com/ → YouTube Data API v3")
sys.exit(1)
print("\n✅ 공유 설정 로드 OK. 다른 도구들이 이 값을 자동으로 사용합니다.")
if __name__ == "__main__":
main()
+46
View File
@@ -0,0 +1,46 @@
# 🧬 1인 기업 OS — 자가 매뉴얼
## 이 폴더는 무엇인가요?
당신의 1인 기업의 두뇌입니다. 7명의 AI 에이전트가 여기서 일합니다.
## 폴더 구조
- `_shared/` — 모든 에이전트가 매번 읽는 공동 메모리
- `identity.md` — 회사 정체성 (이름, 톤, 가치)
- `goals.md` — 목표
- `decisions.md` — 의사결정 로그 (자가학습이 자동 누적)
- `_system.md` — 이 파일
- `_agents/<id>/` — 각 에이전트 개인 공간
- `memory.md` — 자가학습 (자동, append-only)
- `prompt.md` — 페르소나 디테일 (사용자가 편집)
- `config.md` — API 키·시크릿 (`.gitignore`로 보호)
- `sessions/<ts>/` — 세션별 산출물 (자동)
- `_cache/` — API 응답 캐시 (sync 제외)
## 메모리 위계 (충돌 시 우선순위)
1. `decisions.md` — 가장 강한 신뢰
2. `identity.md`
3. `goals.md`
4. 개인 메모리
5. 지식 베이스 (`10_Wiki/`)
## 다른 PC로 옮길 때
1. 새 PC에 Connect AI 설치
2. 👔 모드 ON → "📥 다른 PC에서 가져오기" 선택
3. GitHub URL 입력 → 자동 clone
4. 끝.
## 동기화 정책
- `_shared/`, `_agents/*/memory.md`, `_agents/*/prompt.md`, `sessions/` → git sync ✅
- `_agents/*/config.md`, `_cache/` → git sync ❌ (시크릿·캐시)
## 7명의 에이전트
- 🧭 **CEO** (Chief Executive Agent): 오케스트레이션, 작업 분해, 종합 판단, 다음 액션 결정
- 📺 **YouTube** (Head of YouTube): 유튜브 채널 운영, 영상 기획서(제목·후크·구조), 트렌드 분석, 썸네일 브리프, 업로드 메타데이터, 시청자 유지율 전략
- 📷 **Instagram** (Head of Instagram): 인스타그램 릴스/피드 콘셉트, 캡션, 해시태그 전략, 게시 시간, 스토리, 팔로워 인게이지먼트
- 🎨 **Designer** (Lead Designer): 브랜드 디자인 브리프(컬러·타이포·레퍼런스), 썸네일 컨셉 3안, 비주얼 시스템, 디자인 가이드
- 💻 **Developer** (Lead Engineer): 코드, 자동화 스크립트, API 통합, 웹사이트/봇, 데이터 파이프라인, 디버깅
- 💰 **Business** (Head of Business): 수익화 모델, 가격 전략, 시장·경쟁 분석, ROI/KPI 설계, 비즈니스 의사결정
- 📱 **Secretary** (Personal Assistant): 일정·할 일 관리, 다른 에이전트 작업 요약·텔레그램 보고, 데일리 브리핑, 알림
- ✂️ **Editor** (Video & Content Editor): 영상 편집 디렉션, 컷 구성, B-roll 제안, 자막·타이틀, 스크립트 다듬기, 콘텐츠 폴리싱
- ✍️ **Writer** (Copywriter): 카피라이팅, 영상 스크립트 초안, 인스타 캡션, 블로그 글, 메일 톤앤매너, 후크 작성
- 🔍 **Researcher** (Trend & Data Researcher): 트렌드 리서치, 경쟁사 분석, 데이터 수집·요약, 인용 자료 정리, 사실 확인
+1
View File
@@ -0,0 +1 @@
{}
+12
View File
@@ -0,0 +1,12 @@
# 🎯 공동 목표
## 올해 핵심 목표
- [ ] 게임 개발 1개
## 1개월 내 단기 목표
- _자가학습이 채울 예정_
## 지금 가장 필요한 것
- 30-40대 어른들도 즐겁게 즐길 수 있는 게임을 개발하고 싶어.
> 모든 에이전트가 매번 이 파일을 읽고 일합니다. 회사 설정 모달에서 폼으로도 수정 가능.
+10
View File
@@ -0,0 +1,10 @@
# 🏢 회사 정체성
- **회사 이름:** g1nation
- **한 줄 소개:** PM
- **타깃 청중:** 30대~50대 사회인
- **브랜드 톤:** _자가학습이 채울 예정_
- **금기:** _자가학습이 채울 예정_
> 이 파일은 사용자가 직접 편집하거나, 작업하면서 자가학습으로 채워집니다.
> 채팅 사이드바의 "👔 회사명" 뱃지를 누르면 폼으로 수정할 수도 있어요.
+6
View File
@@ -0,0 +1,6 @@
{
"tasksCompleted": 0,
"knowledgeInjected": 0,
"lastSessionDate": "",
"foundedAt": "2026-05-07"
}