Agency Docs Updater
모든 단계를 순차적으로 자동 실행합니다. 단계가 실패하여 복구할 수 없는 경우에만 일시 중지합니다. 시작하기 전에 references/learnings.md에서 알려진 문제점을 읽어보세요.
구성: 경로는 스킬 루트의 .env 파일에서 읽습니다(.env.example 참조). 기본값은 표준 설정에서 작동합니다. 주요 환경 변수: VAULT_DIR, DOCS_SITE_DIR, YOUTUBE_UPLOADER_DIR, PRESENTATIONS_DIR, SKILLS_REPO_DIR, SKILLS_LOCAL_DIR, ZOOM_CREDENTIALS_DIR, GITHUB_REPO, SITE_DOMAIN.
종속성 (실행 전에 이러한 항목이 있는지 확인하세요):
- zoom — Zoom 녹화 다운로드 (
scripts/zoom_meetings.py) - fathom — Fathom 영상 대체 (
scripts/download_video.py) - nano-banana — 썸네일 오버레이 생성 (
scripts/generate_image.sh) - calendar-sync — 로컬 전용, 캘린더 이벤트 동기화 (
sync.sh) - youtube-uploader — 영상 처리, 업로드 및 YouTube API 인증
0단계: 인수 구문 분석 및 구성 로드
스킬 루트에서 .env를 로드합니다. 그런 다음 args를 공백으로 분할합니다:
- 8자리 토큰 (
YYYYMMDD) →DATE - "yesterday" →
DATE = $(date -v-1d +%Y%m%d) - "today" 또는 누락 →
DATE = $(date +%Y%m%d) - 2자리 토큰 (
NN) 또는lab-NN→LAB_FILTER
후속 단계에서 사용되는 경로에 대한 환경 변수를 확장합니다:
VAULT_DIR="${VAULT_DIR:-$HOME/Brains/brain}"
DOCS_SITE_DIR="${DOCS_SITE_DIR:-$HOME/Sites/agency-docs}"
YOUTUBE_UPLOADER_DIR="${YOUTUBE_UPLOADER_DIR:-$HOME/ai_projects/youtube-uploader}"
SKILLS_REPO_DIR="${SKILLS_REPO_DIR:-$HOME/ai_projects/claude-skills}"
SKILLS_LOCAL_DIR="${SKILLS_LOCAL_DIR:-$HOME/.claude/skills}"
ZOOM_CREDENTIALS_DIR="${ZOOM_CREDENTIALS_DIR:-$HOME/.zoom_credentials}"
PRESENTATIONS_DIR="${PRESENTATIONS_DIR:-$HOME/ai_projects/claude-code-lab}"
GITHUB_REPO="${GITHUB_REPO:-glebis/agency-docs}"
SITE_DOMAIN="${SITE_DOMAIN:-agency-lab.glebkalinin.com}"
1단계: Fathom 대본 찾기
LAB_FILTER가 설정된 경우: ${VAULT_DIR}/${DATE}-claude-code-lab-${LAB_FILTER}.md
비어 있는 경우: glob ${VAULT_DIR}/${DATE}-claude-code-lab-*.md (mtime 기준 가장 최근 항목 선택).
파일이 없는 경우: ${SKILLS_LOCAL_DIR}/calendar-sync/sync.sh를 실행하고, 다시 확인하고, 그래도 없으면 중지합니다.
YAML frontmatter에서 추출하여 저장:
FATHOM_FILE,SHARE_URL,MEETING_TITLE,DATE,LAB_NUMBERVIDEO_NAME=${DATE}-claude-code-lab-${LAB_NUMBER}TRANSCRIPT_LANG= 처음 ~50줄에서 자동 감지 (키릴 문자 비율 > 0.3 →ru, 그 외en)
MEETING_NUMBER 확인: ${DOCS_SITE_DIR}/content/docs/claude-code-internal-${LAB_NUMBER}/meetings/에서 기존 MDX 파일을 확인하여 오늘 날짜의 플레이스홀더를 찾습니다. 발견되면 해당 번호를 사용합니다. 그렇지 않으면 파일 콘텐츠 크기를 확인하여 다음 빈 슬롯을 찾습니다. 0으로 채워진 두 자리 문자열(예: 04)로 저장합니다. 이 변수는 3b, 4b, 5, 6, 8단계에서 사용됩니다.
2단계: 영상 다운로드
${VAULT_DIR}/${VIDEO_NAME}.mp4가 존재하고 1MB보다 크면 건너뜁니다.
참고: Zoom 녹화는 미팅 종료 후 약 15분 정도 처리 시간이 소요될 수 있습니다. Zoom API가 녹화를 반환하지 않으면 Fathom으로 대체하기 전에 기다렸다가 다시 시도하세요.
기본 — Zoom:
python3 ${SKILLS_REPO_DIR}/zoom/scripts/zoom_meetings.py recordings \
--start ${DATE:0:4}-${DATE:4:2}-${DATE:6:2} \
--end $(date -j -v+1d -f %Y%m%d ${DATE} +%Y-%m-%d) \
--show-downloads 2>&1
MP4 URL을 찾은 후:
TOK=$(python3 -c "import json,pathlib; print(json.load(open(pathlib.Path('${ZOOM_CREDENTIALS_DIR}')/'oauth_token.json'))['access_token'])")
curl -L -H "Authorization: Bearer ${TOK}" -o ${VAULT_DIR}/${VIDEO_NAME}.mp4 "${MP4_DOWNLOAD_URL}"
대체 — Fathom (Zoom 녹화가 없는 경우):
cd ${VAULT_DIR} && python3 ${SKILLS_LOCAL_DIR}/fathom/scripts/download_video.py \
"${SHARE_URL}" --output-name "${VIDEO_NAME}"
3단계: YouTube에 업로드
cd ${YOUTUBE_UPLOADER_DIR} && \
python3 process_video.py \
--video ${VAULT_DIR}/${VIDEO_NAME}.mp4 \
--fathom-transcript ${FATHOM_FILE} \
--title "${MEETING_TITLE}" \
--upload
run_in_background: true로 실행합니다 (10-30분). 실패 시: --resume-from upload.
stdout(✓ YouTube video: ...) 또는 processed/metadata/${VIDEO_NAME}.json에서 YOUTUBE_URL을 추출합니다.
URL에서 VIDEO_ID를 추출합니다 (?v= 뒤 또는 마지막 경로 세그먼트).
3a단계: 업로드 확인 (필수)
VIDEO_ID를 추출한 후, 계속 진행하기 전에 YouTube에서 영상이 실제로 존재하는지 확인하세요. 영상이 자동으로 처리에 실패하거나 YouTube의 콘텐츠 검토에 의해 자동 삭제될 수 있습니다.
cd ${YOUTUBE_UPLOADER_DIR} && PYTHONPATH=. python3 -c "
from auth import get_authenticated_service
import sys, time
youtube = get_authenticated_service()
video_id = '${VIDEO_ID}'
# Poll up to 5 minutes for video to become available
for attempt in range(10):
resp = youtube.videos().list(part='status,processingDetails', id=video_id).execute()
if not resp['items']:
if attempt < 9:
print(f'Video not yet available (attempt {attempt+1}/10), waiting 30s...')
time.sleep(30)
continue
print(f'FATAL: Video {video_id} not found after 5 minutes. Upload may have failed.')
sys.exit(1)
status = resp['items'][0]['status']
processing = resp['items'][0].get('processingDetails', {})
upload_status = status.get('uploadStatus', 'unknown')
privacy = status.get('privacyStatus', 'unknown')
rejection = status.get('rejectionReason', None)
print(f'Upload status: {upload_status}, Privacy: {privacy}')
if rejection:
print(f'REJECTED: {rejection}')
sys.exit(1)
if upload_status in ('processed', 'uploaded'):
print(f'✓ Video {video_id} verified OK')
sys.exit(0)
if upload_status == 'failed':
print(f'FATAL: Upload failed — {status.get(\"failureReason\", \"unknown\")}')
sys.exit(1)
print(f'Status: {upload_status}, waiting 30s...')
time.sleep(30)
print('FATAL: Video not ready after 5 minutes')
sys.exit(1)
"
확인에 실패한 경우: 실패한 영상 메타데이터를 삭제하고(rm processed/metadata/${VIDEO_NAME}.json), --resume-from upload로 다시 업로드하고 재확인합니다. 확인되지 않은 VIDEO_ID로 MDX 또는 썸네일 단계를 진행하지 마세요.
4단계를 병렬로 시작 — 요약은 YouTube URL에 의존하지 않습니다.
3b단계: 랩 스타일 썸네일 (필수)
이 단계는 항상 실행 — process_video.py의 일반 썸네일을 브랜드 랩 템플릿으로 교체합니다. 일반 썸네일은 게시에 적합하지 않습니다.
전제 조건: VIDEO_ID를 알고 있어야 합니다 (필요시 3단계가 완료될 때까지 대기).
전체 워크플로는 references/thumbnail-guide.md를 따르세요:
- Nano Banana 오버레이 이미지 생성 (가이드의 프롬프트 패턴에서 주제별 프롬프트 사용)
- 원시 이미지를 읽거나 검사하여 배경색을 확인한 다음, 선 색상을 주황색(#e85d04)으로 다시 칠합니다.
${YOUTUBE_UPLOADER_DIR}/templates/images/lab-meeting.html을 기반으로 임시 HTML 파일(예:/tmp/lab-meeting-${MEETING_NUMBER}.html)을 작성합니다. — 미팅 번호, 주제 히어로 텍스트, 글머리 설명, 날짜를 업데이트합니다. 원본 템플릿을 직접 편집하지 마세요.- Playwright로 1280×720으로 렌더링 →
${YOUTUBE_UPLOADER_DIR}/processed/thumbnails/${VIDEO_NAME}.jpg - 업로드 전에 렌더링된 썸네일을 읽거나 검사하여 레이아웃을 확인합니다.
- YouTube에 업로드: 3단계에서 추출한
VIDEO_ID사용
이 단계를 건너뛰거나 process_video.py 썸네일에 의존하지 마세요.
4단계: 사실 확인된 요약 생성
${FATHOM_FILE}을 읽습니다. ${TRANSCRIPT_LANG} 로 구조화된 요약을 생성합니다:
##섹션 제목, 글머리 기호, 관련 코드 예제- 기술 용어는 영어 유지 (MCP, Skills, Claude Code 등)
- 개인 일정 관련 세부 정보 제외
- MDX용 살균: MDX 컴파일을 손상시킬
<,>, 및 이스케이프되지 않은{문자를 이스케이프 처리합니다.
claude-code-guide 하위 에이전트를 사용하여 Claude Code 기능 주장을 사실 확인합니다(사용 가능한 경우, 에이전트에 접근할 수 없으면 사실 확인 건너뜀). 수정된 요약을 스크래치패드에 summary.md로 저장합니다.
4b단계: YouTube 메타데이터 업데이트
3단계와 4단계가 모두 완료된 후에 실행합니다. 이 단계 전에 VIDEO_ID, MEETING_NUMBER, LAB_NUMBER가 모두 결정되어야 합니다. 설명 형식 및 API 스니펫은 references/youtube-api.md를 참조하세요.
요약에서 YouTube 설명을 생성합니다. 언어에 적합한 템플릿 사용:
TRANSCRIPT_LANG=en인 경우: 영어 레이블 ("In this video:", "Course materials and session notes:")TRANSCRIPT_LANG=ru인 경우: 러시아어 레이블 ("В этом видео:", "Материалы и конспект занятия:")
단일 설명에서 언어를 혼합하지 마세요.
미팅 페이지 URL: https://${SITE_DOMAIN}/claude-code-lab-${LAB_NUMBER}/meetings/${MEETING_NUMBER}
YouTube API를 통해 제목, 설명, 태그를 업데이트한 다음, 비디오를 재생목록 "Claude Code Lab ${LAB_NUMBER}"에 추가합니다 (존재하지 않으면 자동 생성).
5단계: MDX 생성
python3 ${SKILLS_LOCAL_DIR}/agency-docs-updater/scripts/update_meeting_doc.py \
${FATHOM_FILE} "${YOUTUBE_URL}" ${SCRATCHPAD}/summary.md
실행 전: 오늘 날짜에 대한 플레이스홀더 MDX가 이미 있는지 확인합니다 (meetings/에서 grep -l). 있는 경우, -n ${MEETING_NUMBER} --update를 사용하여 대상으로 지정합니다.
실행 후:
- 추가된 Marp 콘텐츠 제거 (요약의 닫는
---이후<!-- _class: lead -->이전의 모든 내용) — MDX는 HTML 주석(<!-- -->), 이스케이프되지 않은<, 및 이스케이프되지 않은{문자에서 손상됩니다. - 프레젠테이션 파일 확인:
${PRESENTATIONS_DIR}/presentations/lab-${LAB_NUMBER}/및${PRESENTATIONS_DIR}/lesson-generator/에서${DATE}와 일치하는 파일을 찾습니다. 발견되면${DOCS_SITE_DIR}/public/${DATE}-claude-code-lab-${LAB_NUMBER}.html로 복사하고 MDX에 링크를 추가합니다. - frontmatter 플레이스홀더(
[Название встречи],[Краткое описание встречи],[Дата встречи])를 교체합니다. TRANSCRIPT_LANG=en인 경우, 영어 레이블로 MDX를 완전히 다시 작성합니다 — 스크립트는 기본적으로 러시아어를 사용하며 번역 대체 기능이 손상된 혼합 언어 출력을 생성합니다.- 확인:
cd ${DOCS_SITE_DIR} && npm run build 2>&1 | tail -5
6단계: 커밋 및 푸시
파이프라인 파일만 스테이징하세요 — 절대 git add .를 사용하지 마세요:
cd ${DOCS_SITE_DIR}
git fetch origin main
BEHIND=$(git rev-list --count HEAD..origin/main)
if [ "$BEHIND" -gt 0 ]; then
git stash push -m "agency-docs-updater: temp stash"
git pull --rebase origin main
git stash pop || true
fi
git add content/docs/claude-code-internal-${LAB_NUMBER}/meetings/${MEETING_NUMBER}.mdx
# Only stage presentation HTML if it was copied
[ -f public/${DATE}-claude-code-lab-${LAB_NUMBER}.html ] && git add public/${DATE}-claude-code-lab-${LAB_NUMBER}.html
git commit -m "Add Lab ${LAB_NUMBER} Meeting ${MEETING_NUMBER}"
git push
7단계를 위해 COMMIT_HASH=$(git rev-parse HEAD)를 저장합니다.
7단계: Vercel 배포 대기
TIMEOUT=300; ELAPSED=0
until [ "$(gh api repos/${GITHUB_REPO}/commits/${COMMIT_HASH}/status --jq '.state' 2>/dev/null || echo 'pending')" != "pending" ]; do
sleep 15; ELAPSED=$((ELAPSED+15))
[ "$ELAPSED" -ge "$TIMEOUT" ] && echo "Deploy timeout after ${TIMEOUT}s" && break
done
DEPLOY_STATE=$(gh api repos/${GITHUB_REPO}/commits/${COMMIT_HASH}/status --jq '.state')
echo "Deploy state: ${DEPLOY_STATE}"
run_in_background: true로 실행합니다. 상태가 failure 또는 error인 경우: Vercel 로그 확인(vercel logs), 로컬에서 수정, 다시 푸시, 이 단계 다시 시작.
8단계: 브라우저에서 확인
브라우저(Chrome 자동화 도구 또는 수동)에서 https://${SITE_DOMAIN}/claude-code-lab-${LAB_NUMBER}/meetings/${MEETING_NUMBER}를 엽니다. YouTube 임베드가 보이는지 확인합니다. 보이지 않는 경우: VIDEO_ID 확인, YouTube 처리 대기 또는 다시 업로드합니다.
파이프라인 보고서
완료 후 보고: Fathom 경로, 영상 경로, YouTube URL, MDX 경로, 커밋 해시, 배포 상태, 임베드 확인.