================================================================ 팟캐스트 자동 배포 시스템 구축 문서 (as-built) 작성: 2026-06-10 KST, Claude Code (Fable 5) 대상 시스템: 오늘 AI 뉴스 / 중국 AI·로봇·반도체 브리핑 팟캐스트 ================================================================ 0. 이 문서의 용도 ---------------------------------------------------------------- 2026-06-10에 계획·설계·구축을 한 번에 끝낸 팟캐스트 자동 배포 시스템의 전체 기록입니다. 유지·수선·보수 때 이 문서 하나로 시스템 전체를 복원할 수 있도록 접속 정보, 파일 위치, 스크립트 전문, 설계 이유, 구축 중 발생한 실수와 해결까지 전부 담았습니다. 사본 위치: - 원본: ~/iCloud/ai_workspace/팟캐스트_자동배포_시스템_구축문서_20260610.txt - 요약(에이전트용): ~/iCloud/AI-Agent-Harness/harness/facts/podcast_feed_automation_system_20260610.md - 현장(우분투): /home/ukimkj/podcast/README_운영문서.txt - 사전 연구 보고서: ~/iCloud/ai_workspace/podcast_feed_automation_research_20260610.txt 1. 시스템 개요 ---------------------------------------------------------------- 한 줄 요약: 매일 만들어지는 뉴스 영상의 음성(mp3)을 그대로 팟캐스트 에피소드로 재활용한다. 우분투가 에피소드를 쌓아 feed.xml(RSS)을 다시 쓰고 라이트세일 웹서버로 밀어 넣으면, 애플·스포티파이가 그 피드를 주기적으로 읽어 간다. 핵심 원리: 팟캐스트는 mp3 주소가 담긴 RSS XML 한 장이다. 서버가 플랫폼에 보내는 게 아니라 플랫폼이 가져가는 구조라서, 피드 URL을 한 번 등록한 뒤에는 사람 손이 필요 없다. 쇼 구성 (피드 2개로 분리, 청취층이 달라서): - 오늘 AI 뉴스 https://kimkj.com/podcast/daily-ai/feed.xml - 중국 AI·로봇·반도체 브리핑 https://kimkj.com/podcast/china-ai/feed.xml 2. 구성 요소와 접속 정보 ---------------------------------------------------------------- [우분투 서버: 생산·발행 담당] - 접속: ssh ukimkj@100.92.148.124 (테일스케일, 맥의 ~/.ssh/id_ed25519) - 하드웨어: 2019 Xeon Mac Pro, RAM 256GB, SSD 3TB - 작업 폴더: /home/ukimkj/podcast/ - 원천 데이터(영상 파이프라인, 절대 이 시스템에서 수정하지 않음): /home/ukimkj/Documents/DailyAINewsVideoAutomation/runs/YYYY-MM-DD/ /home/ukimkj/Documents/ChinaAIRobotSemiTelecomVideoAutomation/runs/YYYY-MM-DD/ - 음성: audio/final_audio_slide_synced.mp3 (약 100kbps, 8~14분, 7~10MB) - 제목·설명: reports/youtube_upload_result.json 의 raw.snippet.title / .description - 게시글 링크: reports/kimkj_post_result.json 의 post_verification.url (퍼센트 인코딩본) [라이트세일: 공개 서빙 담당] - 인스턴스: WordPress-Lightsail-20260508 (서울 ap-northeast-2, Zone A) 2GB RAM / 2 vCPU / 60GB SSD, 2026-05-08 생성 - 고정 IP: 43.202.123.207 - OS: Debian 12 계열. 신형 라이트세일 워드프레스 블루프린트라서 SSH 사용자가 bitnami가 아니라 admin 이다. (구형 /opt/bitnami 구조 아님) - 웹서버: Apache2, DocumentRoot /var/www/html (워드프레스가 직접 설치됨) - 팟캐스트 폴더: /var/www/html/podcast/ (소유자 admin:admin) - SSH 접속(우분투에서): ssh -i ~/.ssh/lightsail_podcast admin@43.202.123.207 - 비상 접속: AWS 콘솔 → Lightsail → 인스턴스의 웹 SSH 터미널 https://lightsail.aws.amazon.com/ls/remote/ap-northeast-2/instances/WordPress-Lightsail-20260508/terminal?protocol=ssh [DNS / 네트워크] - kimkj.com DNS는 Cloudflare (aurora/vick.ns.cloudflare.com, 2026-06-05 이전 완료) - kimkj.com은 Cloudflare 프록시를 탄다. dig kimkj.com을 치면 104.21.x / 172.67.x 같은 Cloudflare IP가 나오는 게 정상이다. - 따라서 SSH·rsync는 반드시 원IP 43.202.123.207로 직접 간다. kimkj.com으로 ssh를 시도하면 안 된다. - mp3·feed.xml은 Cloudflare 프록시를 통해 서빙된다. 전송 압축 때문에 curl -I의 content-length가 실제 파일보다 작게 보일 수 있다(정상). - byte-range 요청(애플 요구사항)은 206 응답으로 정상 동작 확인됨. [AWS 참고] - AWS 계정 302825584161 (kimkj008@gmail.com 루트) - 이 맥의 AWS MCP는 IAM 사용자 ses-newsletter 자격을 쓰는데 Lightsail 권한이 없다. 라이트세일 콘솔 작업은 브라우저 로그인으로 한다. - Route53 hosted zone 삭제 금지 시한(2026-08-05)은 별도 fact 문서 참조. 3. 매일 자동 흐름 (타임라인, KST) ---------------------------------------------------------------- 09:10 Daily AI 영상 파이프라인 시작 (기존 cron, 건드리지 않음) 09:20쯤 Daily 유튜브 업로드 완료, runs/오늘날짜/ 산출물 확정 09:40 China AI 영상 파이프라인 시작 10:20 [이 시스템] publish_podcast.sh 1차 실행 - 오늘 날짜 mp3 존재 확인 → 없으면 "오디오 없음, 건너뜀" 기록 후 통과 - 있으면 ep/YYYYMMDD.mp3 복사, ffprobe로 길이(초) 측정 - youtube_upload_result.json에서 제목·설명, kimkj_post_result.json에서 링크를 뽑아 ep/YYYYMMDD.json 생성 - build_feed.py로 feed.xml 전체 재생성 - rsync로 라이트세일 /var/www/html/podcast/에 동기화 12:00 publish_podcast.sh 2차 실행 (1차에서 누락된 쇼 재시도. 멱등이라 안전) 이후 애플·스포티파이가 피드를 폴링해 새 에피소드 반영 (보통 수십 분~수 시간) 영상이 안 만들어진 날은 팟캐스트도 자동으로 쉰다. 별도 조치 불필요. cron 등록 내용 (ukimkj 계정, crontab -l로 확인): 20 10 * * * /home/ukimkj/podcast/publish_podcast.sh >> /home/ukimkj/podcast/cron.log 2>&1 0 12 * * * /home/ukimkj/podcast/publish_podcast.sh >> /home/ukimkj/podcast/cron.log 2>&1 4. 디렉토리·파일 상세 ---------------------------------------------------------------- [우분투 /home/ukimkj/podcast/] ├── publish_podcast.sh 매일 cron이 부르는 게시 스크립트 (실행 권한 있음) ├── build_feed.py feed.xml 생성기 (표준 라이브러리만, 의존성 없음) ├── make_episode_meta.py 에피소드 메타(.json) 추출기 ├── patch_links_oneoff.py 과거 에피소드에 링크 소급 주입용 1회성 (보존만) ├── publish.log 게시 이력 (사람이 읽는 로그) ├── cron.log cron 표준출력/에러 ├── daily-ai/ │ ├── show.json 쇼 메타 (아래 스키마) │ ├── cover.jpg 3000x3000 커버 (q88 JPG, 약 0.6MB) │ ├── feed.xml 자동 생성물 (직접 수정 금지) │ └── ep/ │ ├── 20260610.mp3 에피소드 음원 (날짜 8자리 파일명, ASCII 필수) │ └── 20260610.json 에피소드 메타 └── china-ai/ 동일 구조 (cover.jpg 약 1.1MB) [라이트세일 /var/www/html/podcast/] - 위 daily-ai/, china-ai/가 rsync로 그대로 복제됨 (정본은 우분투 쪽) - 라이트세일 쪽은 절대 직접 수정하지 않는다. 고치면 다음 rsync 때 덮인다. [show.json 스키마] (예: daily-ai) { "title": "오늘 AI 뉴스", "description": "매일 아침 주요 AI 뉴스를 10분 안에 정리하는 데일리 브리핑. 진행: 김경진 변호사", "base_url": "https://kimkj.com/podcast/daily-ai", "guid_prefix": "kimkj-daily-ai", "author": "김경진", "email": "kimkj008@gmail.com", "language": "ko", "category": "News", "subcategory": "Tech News" } china-ai는 title "중국 AI·로봇·반도체 브리핑", base_url .../china-ai, guid_prefix "kimkj-china-ai"만 다르고 나머지 동일. [에피소드 .json 스키마] (publish가 자동 생성) { "duration": 573, # 초 단위 (ffprobe 측정값) "title": "클로드 Fable 5, ...", # 유튜브 제목 그대로 "description": "...", # 유튜브 설명 그대로 (피드에서 3900자 절단) "link": "https://kimkj.com/%EC%98%A4...uid=5832" # kimkj.com 게시글 } title/description/link가 없으면 build_feed가 "쇼제목 YYYY-MM-DD"로 대체한다. 과거 날짜를 소급 게시하면 그 날짜 reports에서 자동으로 뽑아온다. 5. 스크립트 전문 (원본 보존용. 유실 시 이대로 복원) ---------------------------------------------------------------- ### /home/ukimkj/podcast/publish_podcast.sh #!/bin/bash # 영상 파이프라인이 만든 mp3를 팟캐스트 폴더에 싣고 feed를 다시 만들어 Lightsail로 민다. # 사용법: publish_podcast.sh [YYYY-MM-DD] (날짜 생략 시 오늘) set -euo pipefail BASE=/home/ukimkj/podcast RUN_DATE=${1:-$(date +%Y-%m-%d)} DAY=${RUN_DATE//-/} LOG=$BASE/publish.log REMOTE="admin@43.202.123.207" # 라이트세일 SSH 사용자(admin, 데비안 기반 이미지) REMOTE_DIR="/var/www/html/podcast" # 웹루트 확정 후 수정 SSH_OPTS="-i $HOME/.ssh/lightsail_podcast -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10" note() { echo "$(date '+%F %T') $*" >> "$LOG"; } publish_one() { local show=$1 run_root=$2 local src="$run_root/runs/$RUN_DATE/audio/final_audio_slide_synced.mp3" local yt_json="$run_root/runs/$RUN_DATE/reports/youtube_upload_result.json" local dst="$BASE/$show/ep/$DAY.mp3" [ -f "$src" ] || { note "$show $RUN_DATE: 오디오 없음, 건너뜀"; return 0; } [ -f "$dst" ] && { note "$show $RUN_DATE: 이미 게시됨"; return 0; } cp "$src" "$dst" local dur dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$dst" | cut -d. -f1) python3 "$BASE/make_episode_meta.py" "$yt_json" "$BASE/$show/ep/$DAY.json" "$dur" "$run_root/runs/$RUN_DATE/reports/kimkj_post_result.json" python3 "$BASE/build_feed.py" "$BASE/$show" note "$show $RUN_DATE: 게시 (길이 ${dur}s)" } publish_one daily-ai /home/ukimkj/Documents/DailyAINewsVideoAutomation publish_one china-ai /home/ukimkj/Documents/ChinaAIRobotSemiTelecomVideoAutomation if rsync -az -e "ssh $SSH_OPTS" "$BASE/daily-ai" "$BASE/china-ai" "$REMOTE:$REMOTE_DIR/" 2>>"$LOG"; then note "rsync 완료" else note "rsync 실패 (키 미등록/경로/네트워크). 다음 실행에서 재시도" fi ### /home/ukimkj/podcast/build_feed.py #!/usr/bin/env python3 """ep/ 폴더의 mp3와 짝지은 .json 메타로 podcast RSS(feed.xml)를 다시 만든다.""" import json import sys from datetime import datetime, timedelta, timezone from email.utils import format_datetime from pathlib import Path from xml.sax.saxutils import escape KST = timezone(timedelta(hours=9)) HEAD = """ {title} {description} {base} {language} {author} {author}{email} episodic false """ ITEM = """ {title} {description} {link} {guid} {pubdate} {duration} false """ def main(show_dir): show_dir = Path(show_dir) cfg = json.loads((show_dir / "show.json").read_text(encoding="utf-8")) base = cfg["base_url"].rstrip("/") items = [] for mp3 in sorted((show_dir / "ep").glob("*.mp3"), reverse=True): meta_path = mp3.with_suffix(".json") meta = json.loads(meta_path.read_text(encoding="utf-8")) if meta_path.exists() else {} pub = datetime.strptime(mp3.stem, "%Y%m%d").replace(hour=10, tzinfo=KST) title = meta.get("title") or "{} {}".format(cfg["title"], pub.strftime("%Y-%m-%d")) desc = (meta.get("description") or title)[:3900] dur = "" if meta.get("duration"): dur = " {}\n".format(meta["duration"]) link = "" if meta.get("link"): link = " {}\n".format(escape(meta["link"])) items.append(ITEM.format( title=escape(title), description=escape(desc), base=base, fname=mp3.name, size=mp3.stat().st_size, guid="{}-{}".format(cfg["guid_prefix"], mp3.stem), pubdate=format_datetime(pub), duration=dur, link=link)) xml = HEAD.format( title=escape(cfg["title"]), description=escape(cfg["description"]), base=base, language=cfg.get("language", "ko"), author=escape(cfg["author"]), email=cfg["email"], category=cfg.get("category", "News"), subcategory=cfg.get("subcategory", "Tech News"), ) + "".join(items) + " \n\n" (show_dir / "feed.xml").write_text(xml, encoding="utf-8") print("OK {}: {} episodes".format(show_dir.name, len(items))) if __name__ == "__main__": main(sys.argv[1]) ### /home/ukimkj/podcast/make_episode_meta.py #!/usr/bin/env python3 """youtube_upload_result.json에서 제목·설명을 뽑아 에피소드 메타 .json을 만든다.""" import json import sys yt_path, out_path, dur = sys.argv[1], sys.argv[2], int(sys.argv[3]) post_path = sys.argv[4] if len(sys.argv) > 4 else None meta = {"duration": dur} try: with open(yt_path, encoding="utf-8") as f: snip = json.load(f)["raw"]["snippet"] if snip.get("title"): meta["title"] = snip["title"] if snip.get("description"): meta["description"] = snip["description"] except Exception: pass if post_path: try: with open(post_path, encoding="utf-8") as f: post = json.load(f) link = post.get("post_verification", {}).get("url") or post.get("url") if link: meta["link"] = link except Exception: pass with open(out_path, "w", encoding="utf-8") as f: json.dump(meta, f, ensure_ascii=False, indent=1) 6. 피드 규격과 불변 규칙 (어기면 사고 나는 것들) ---------------------------------------------------------------- 1) guid는 영구 불변. 형식 "kimkj-daily-ai-20260610". guid를 바꾸면 모든 플랫폼에서 같은 에피소드가 중복 등록된다. build_feed.py의 guid 줄과 show.json의 guid_prefix를 절대 바꾸지 않는다. 2) mp3 파일명과 URL은 ASCII만 (날짜 8자리 파일명 유지). 제목·설명의 한글은 자유. 3) feed.xml은 항상 build_feed.py가 만든다. 손으로 고치지 않는다. 고치고 싶으면 show.json이나 에피소드 .json을 고치고 재생성한다. 4) base_url(피드 URL)은 플랫폼에 등록된 주소이므로 함부로 바꾸지 않는다. 옮겨야 할 때는 301 리다이렉트 + 각 플랫폼에서 피드 URL 변경 절차를 따로 밟아야 한다. 5) enclosure URL(오디오 위치)은 guid만 유지되면 바꿔도 안전하다. (나중에 R2로 옮길 때 이 성질을 쓴다) 6) 날짜 파일명이 pubDate를 결정한다(해당일 10:00 KST로 기록). 7) RSS 검증 기준: RSS 2.0 + itunes 네임스페이스, RFC 2822 날짜, HTTPS, byte-range(206) 지원. 현재 전부 충족. 7. 커버 아트 제작·교체 절차 ---------------------------------------------------------------- 현재 커버: ~/iCloud/ai_workspace/podcast_covers/ - daily_cover.jpg / china_cover.jpg (3000x3000, JPG q88) - 배경 원본: daily_bg.png / china_bg.png (Codex 생성, 3000x3000) - 합성 스크립트: compose_covers.py 제작 방식 (2단계, 한글 깨짐 방지 핵심): 1) 배경은 Codex CLI 이미지 생성으로 만든다. 글자 없는 배경만 시킨다. codex exec --skip-git-repo-check --sandbox workspace-write \ -C ~/iCloud/ai_workspace/podcast_covers "이미지 생성 도구로 ... NO text ..." (codex features list에서 image_generation이 stable/true인지 확인) 2) 한글 타이틀은 Pillow로 얹는다(AI 이미지 생성은 한글을 자주 깨뜨림). 폰트: /System/Library/Fonts/AppleSDGothicNeo.ttc 인덱스: 0 Regular, 2 Medium, 4 SemiBold, 6 Bold (Bold가 최고 두께, 타이틀은 stroke_width로 두께 보강) 실행: source ~/ai_workspace/venv/bin/activate && python3 compose_covers.py 교체 절차: 1) 새 cover.jpg 제작 (3000x3000, RGB, JPG/PNG, 1400px 이상 필수) 2) scp 새파일 ukimkj@100.92.148.124:~/podcast/daily-ai/cover.jpg 3) ssh ukimkj@... '~/podcast/publish_podcast.sh' (rsync로 전파) 4) 플랫폼 반영은 다음 폴링 때 자동. 애플은 Podcasts Connect에서 "새로 고침"을 누르면 빨라진다. 8. SSH 키 체계와 복구 절차 ---------------------------------------------------------------- [현재 키 체계] - 맥 → 우분투: ~/.ssh/id_ed25519 (기존 체계 그대로) - 우분투 → 라이트세일: ~/.ssh/lightsail_podcast (ed25519, 암호 없음) 코멘트: ukimkj-podcast-to-lightsail-20260610 공개키가 라이트세일 /home/admin/.ssh/authorized_keys 2번째 줄에 등록됨 (1번째 줄은 라이트세일 기본 키) - 비밀키는 우분투에만 존재. 어떤 문서·채팅에도 출력하지 않는다. [키 분실·교체 시 복구 절차] 1) 우분투에서 새 키 생성: ssh-keygen -t ed25519 -f ~/.ssh/lightsail_podcast -N "" -C "ukimkj-podcast-to-lightsail-YYYYMMDD" 2) 공개키 출력: cat ~/.ssh/lightsail_podcast.pub 3) 브라우저로 AWS 콘솔 로그인(kimkj008@gmail.com 루트, MFA) → Lightsail → WordPress-Lightsail-20260508 → 웹 SSH 터미널 열기 4) 터미널에서: echo '공개키 한 줄' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys 5) 우분투에서 테스트: ssh -i ~/.ssh/lightsail_podcast admin@43.202.123.207 'echo OK' 주의: AWS 콘솔 로그인 세션은 브라우저 프로필 단위다. 자동화 도구가 조작하는 탭과 같은 프로필에서 로그인해야 세션이 공유된다. 9. 플랫폼 등록 내역 (2026-06-10 완료분) ---------------------------------------------------------------- [Apple Podcasts] 상태: 두 쇼 모두 "이용 가능" (게시 완료) - 콘솔: https://podcastsconnect.apple.com (Apple ID: kimkj008@gmail.com) - Podcasts Connect 계정 이름 "김경진", 유형 "개인" - 오늘 AI 뉴스: 프로그램 ID 1896903562 https://podcasts.apple.com/us/podcast/id1896903562 - 중국 AI·로봇·반도체 브리핑: 프로그램 ID 1896903725 https://podcasts.apple.com/us/podcast/id1896903725 - 설정값: 카테고리 뉴스>IT 뉴스(피드에서 읽음), 언어 한국어, 업데이트 빈도 "매일"(게시 필수값), 콘텐츠 권한 "타사 콘텐츠 없음", 연락처 김경진 / kimkj008@gmail.com - 새 에피소드 반영이 늦으면: Connect 쇼 페이지의 "새로 고침" 클릭 [Spotify] 상태: 두 쇼 제출 완료 (제출 후 수 시간 내 게시) - 콘솔: https://creators.spotify.com (로그인: "Google로 계속하기" → kimkj008@gmail.com) - 오늘 AI 뉴스: https://open.spotify.com/show/033vNrQa0WsCM1oxlPi0OL - 중국 브리핑: https://open.spotify.com/show/033vNyj0ndm0lcxM30i58x - 등록 절차 기록: addpodcast → "기존에 만든 프로그램 찾기" → "Somewhere else" → 피드 URL → 이메일 인증(8자리 코드, kimkj008@gmail.com 수신) → 국가 South Korea / 언어 Korean / 호스팅 기타·모름 / 카테고리 News & Politics → 제출 - 쇼를 더 추가할 때도 같은 절차. 인증 코드는 쇼마다 새로 온다. [Amazon Music] 상태: 보류 - podcasters.amazon.com 등록 시도 중 아마존 계정(kimkj008@gmail.com) 비밀번호 재설정이 "이름 확인" 단계 시도 초과로 잠김 (2026-06-10). 계정에 등록된 이름이 김경진 / KYUNGJIN KIM 둘 다 아니었음. - 재시도: 하루 이상 지난 뒤 다시 비밀번호 찾기, 또는 평소 쓰는 다른 아마존 계정으로 등록 (피드 등록은 계정 불문 가능) [YouTube Music] 상태: 보류 (후속 작업 칩 생성됨) - RSS 제출 방식은 채널에 같은 내용이 두 번 올라가므로 금지. - 올바른 경로: 영상 업로드 후 재생목록에 자동 추가하는 단계를 파이프라인에 붙이고(playlistItems.insert, OAuth scope 확인 필요), 그 재생목록을 Studio에서 "팟캐스트로 설정"한다. - 채널: 김경진과 인공지능 @kimkj008 (UCaVYAfAJ2AmSRxjKL3fFPJQ) - 참고: 기존 "AI LLM" 재생목록(37편)이 이미 팟캐스트로 지정돼 있음. [국내 플랫폼 조사 결론 (2026-06 기준)] - 팟빵: 외부 RSS 등록 중단, 직접 업로드 강제. 공식 API 없음 → 보류 - 네이버 오디오클립: 2025-12-31 서비스 종료 → 불가 10. 정상 동작 확인 명령 모음 (복사해서 그대로 실행) ---------------------------------------------------------------- # 1) 우분투 게시 로그 확인 ssh ukimkj@100.92.148.124 'tail -20 ~/podcast/publish.log' # 2) 오늘 에피소드가 쌓였는지 ssh ukimkj@100.92.148.124 'ls -la ~/podcast/daily-ai/ep/ | tail -5; ls -la ~/podcast/china-ai/ep/ | tail -5' # 3) 공개 피드가 살아 있는지 + 에피소드 수 curl -s https://kimkj.com/podcast/daily-ai/feed.xml | grep -c "" curl -s https://kimkj.com/podcast/china-ai/feed.xml | grep -c "" # 4) mp3 byte-range(애플 요건, 206이면 정상) curl -sI -H "Range: bytes=0-1023" https://kimkj.com/podcast/daily-ai/ep/20260610.mp3 | head -3 # 5) 애플 디렉토리 등재·에피소드 수 (lookup API) curl -s "https://itunes.apple.com/lookup?id=1896903562&country=us" curl -s "https://itunes.apple.com/lookup?id=1896903725&country=us" # 6) 피드 정밀 검증 (브라우저) https://castfeedvalidator.com/?url=https://kimkj.com/podcast/daily-ai/feed.xml # 7) 수동 게시 (특정 날짜 소급) ssh ukimkj@100.92.148.124 '~/podcast/publish_podcast.sh 2026-06-09' 11. 장애 대응 안내 (증상별) ---------------------------------------------------------------- [증상 A] 앱에 새 에피소드가 안 뜬다 1. publish.log 확인 → "오디오 없음, 건너뜀"이면 영상 파이프라인 문제. (팟캐스트 시스템은 정상. 영상 쪽 run-news-video.sh 로그를 본다) 2. "게시 (길이 ...s)"는 있는데 "rsync 완료"가 없으면 → 증상 B. 3. 둘 다 정상인데 앱에만 없으면 → 플랫폼 폴링 지연. 몇 시간 기다리거나 애플은 Connect에서 "새로 고침". 공개 피드(위 10-3)에 새 회차가 보이면 이 시스템 책임 구간은 정상이다. [증상 B] rsync 실패가 반복된다 1. 우분투에서 키 테스트: ssh -i ~/.ssh/lightsail_podcast admin@43.202.123.207 'echo OK' 2. Permission denied → 라이트세일 authorized_keys에서 공개키가 사라진 것. 8장 복구 절차로 재등록. 3. Connection timed out → 라이트세일이 꺼졌거나 IP 변경. AWS 콘솔에서 인스턴스 상태·고정IP 확인. IP가 바뀌었으면 publish_podcast.sh의 REMOTE와 Cloudflare A 레코드 둘 다 수정. 4. 디스크 가득참 → ssh admin@... 'df -h /' 확인. [증상 C] 피드가 깨졌다 (validator 에러) 1. 우분투 원본 확인: python3 ~/podcast/build_feed.py ~/podcast/daily-ai 에러가 나면 최근 추가된 에피소드 .json이 깨진 것. 해당 날짜 .json을 지우고 publish를 다시 돌리면 reports에서 재추출한다. 2. XML 특수문자 문제는 escape()가 처리하므로 보통 .json 손상이 원인. [증상 D] 특정 에피소드만 잘못됐다 (제목/음원 교체) 1. ssh ukimkj@... 후 해당 ep/YYYYMMDD.json 수정(제목·설명·링크) 또는 ep/YYYYMMDD.mp3 교체 2. python3 ~/podcast/build_feed.py ~/podcast/<쇼> 3. ~/podcast/publish_podcast.sh (rsync 전파) guid가 그대로면 플랫폼에서 같은 에피소드가 갱신된다. [증상 E] 에피소드를 내리고 싶다 1. ep/YYYYMMDD.mp3 와 .json 삭제 → build_feed → publish 피드에서 빠지면 플랫폼에서도 곧 사라진다. mp3 파일 자체를 라이트세일에서도 지우려면 rsync에 --delete가 없으므로 ssh admin@...로 직접 삭제. [증상 F] Cloudflare가 플랫폼 크롤러를 막는 것 같다 - 신호: 피드는 브라우저에서 열리는데 애플/스포티파이가 갱신 실패를 표시 - 조치: Cloudflare 대시보드 → kimkj.com → Security → /podcast/* 경로에 WAF/Bot Fight 예외 규칙 추가. (2026-06-10 현재는 문제 없음. validator가 Cloudflare 감지 경고를 띄우지만 실동작 정상 확인됨) [증상 G] 라이트세일 월 전송량 초과 추세 - 점검: Lightsail 콘솔 → 인스턴스 → Metrics → Outgoing network - 기준: 에피소드 약 8MB, 구독자 100명이면 월 약 50GB. 플랜 한도 (1~2TB)의 절반을 넘기 시작하면 13장 R2 이전을 실행. 12. 유지보수 레시피 (자주 할 일) ---------------------------------------------------------------- - 쇼 설명·제목 바꾸기: show.json 수정 → build_feed → publish. (쇼 "제목" 변경은 플랫폼 혼란을 부르니 신중히) - 과거 에피소드 일괄 소급: for d in 날짜들; do publish_podcast.sh $d; done - 에피소드 보존 정책: 현재 전부 보존. 연 약 6GB씩 증가. 60GB 디스크 기준 수년 여유. 줄이고 싶으면 ep/ 오래된 파일 삭제 후 build_feed. (피드에서 빠진 과거 회차는 새 구독자에게 안 보이게 됨) - 새 쇼 추가: ~/podcast/새쇼/{show.json, cover.jpg, ep/} 만들고 publish_podcast.sh에 publish_one 한 줄 + rsync 대상 추가. 플랫폼 등록은 9장 Spotify/Apple 절차 반복. - 우분투 서버 교체 시: ~/podcast 통째 복사 + crontab 2줄 + 원천 파이프라인 경로만 맞추면 끝. 라이트세일 쪽은 손댈 것 없음. - 라이트세일 인스턴스 교체 시: 새 서버 웹루트에 /podcast 디렉토리 (admin 소유) + authorized_keys 등록 + REMOTE IP 수정 + DNS 전환. 13. 남은 작업과 확장 로드맵 ---------------------------------------------------------------- [당장 남은 것] 1) Amazon Music 등록: 계정 잠금 해제 후 podcasters.amazon.com에서 피드 2개 제출 (5분 작업, 9장 참조) 2) YouTube Music: 작업 칩 "유튜브 뉴스영상 재생목록 자동 추가 구현" 실행 → 완료 후 Studio에서 재생목록 "팟캐스트로 설정" 3) 스포티파이 게시 확인: 제출 후 수 시간 내 두 링크가 열리는지 확인 [확장 옵션 (필요해질 때)] - Cloudflare R2 이전 (전송량 한도 대비): 1. R2 버킷 생성, 커스텀 도메인 audio.kimkj.com 연결 (CF 계정 보유) 2. rclone으로 ep/*.mp3 동기화 단계를 publish에 추가 3. build_feed.py의 enclosure URL만 audio.kimkj.com으로 변경 (guid 불변 → 구독자 무영향) - 다운로드 통계: enclosure URL 앞에 https://op3.dev/e/ 를 붙이면 무료 오픈 통계(OP3). build_feed.py 한 줄 수정. - kimkj.com 게시글에 오디오 플레이어 삽입: kboard-audio-player 스킬과 결합 가능 (mp3 URL은 이미 공개돼 있음) - 유튜브 영상 설명에 "팟캐스트로 듣기" 링크 자동 추가: derive_youtube_pack 단계에 두 쇼 링크 삽입 - 주간 합본 에피소드: 일주일치 mp3를 ffmpeg concat으로 묶어 토요일에 발행하는 publish 변형 스크립트 14. 보안 원칙 ---------------------------------------------------------------- - 비밀키(~/.ssh/lightsail_podcast)는 우분투에만 둔다. 채팅·문서· 로그 어디에도 비밀키 본문을 출력하지 않는다. (공개키는 무방) - 라이트세일 키 등록은 추가(append)만 한다. 기존 키를 지우지 않는다. - AWS 루트 계정의 액세스 키는 만들지 않는다 (IAM 보안 자격 증명 페이지의 "액세스 키 만들기"를 누르지 말 것). - 플랫폼 로그인·이중인증·OTP 입력은 항상 사람이 직접 한다. Gmail로 온 인증 코드를 읽어 주는 것까지만 자동화한다. - 이 시스템은 결제·삭제·DNS 변경을 하지 않는다. 그런 작업은 별도 확인 후 진행한다. 15. 2026-06-10 구축 작업 로그 (계획 → 설계 → 건설 전 과정) ---------------------------------------------------------------- [1단계: 연구 (오후 1시대)] - 웹 조사로 2026-06 기준 플랫폼 정책 확정: 애플·스포티파이·아마존 = 외부 RSS 등록 가능(무료), 유튜브 = RSS 제출 가능하나 기존 영상과 중복되므로 재생목록 방식 채택, 팟빵 = 외부 RSS 중단(직접 업로드 강제), 네이버 오디오클립 = 종료. - 우분투 실사로 재료 확인: 대본 JSON이 아니라 reports/youtube_upload_result.json에 완성된 제목·설명이 있음을 발견. mp3는 100kbps 573초(약 7MB)로 팟캐스트 규격에 그대로 적합. - 설계 결정: 피드 2개 분리(청취층 상이), 1단계 호스팅은 라이트세일 (구독자 100명 기준 월 48GB로 한도 내), 성장 시 R2 이전 경로 확보, guid는 "prefix-날짜"로 영구 고정, 제목·설명·링크는 기존 산출물 재활용. - 산출물: 연구 보고서 podcast_feed_automation_research_20260610.txt [2단계: 커버 아트 (14:00~14:10)] - codex features list에서 image_generation이 stable/true임을 확인. - Codex로 글자 없는 배경 2장 생성(3000x3000, 네이비/다크레드 테마, 하단 3분의 1 비워달라고 지시) → 성공. - 한글 깨짐을 피하려고 타이틀은 Pillow 합성으로 분리. AppleSDGothicNeo.ttc 인덱스 조사(0,2,4,6...) 후 Bold+stroke 사용. - 1차 합성에서 중국 커버의 골드 바가 제목과 겹침 → y좌표 1600→1530 수정 후 재합성. 우분투 쇼 폴더로 전송. [3단계: 게시 파이프라인 구축 (14:06~14:08)] - 스크립트 4종을 맥에서 작성해 scp로 우분투 ~/podcast에 배치. - 최근 10일 소급 게시 실행: 오늘 AI 뉴스 9편(6/2, 6/9는 원천 오디오 없음), 중국 브리핑 5편(6/6 시작). 실제 영상 제목·설명이 피드에 들어간 것 확인. - cron 2건(10:20, 12:00) 등록. 이 시점 rsync는 키 미등록으로 의도된 실패(로그에 기록되고 파이프라인은 통과 = 설계대로). [4단계: SSH 키와 배포 경로 (14:10~14:17)] - 조사: 맥 ~/.ssh, 셸 히스토리, iCloud 인덱스 어디에도 라이트세일 키 없음. AWS MCP는 ses-newsletter IAM이라 Lightsail 권한 없음. → 경로 결정: 브라우저로 콘솔 → 웹 SSH 터미널 → 공개키 등록. - dig로 kimkj.com이 Cloudflare 프록시임을 확인 → SSH는 원IP 직결로 설계. - 우분투에서 전용 키 생성(lightsail_podcast). 콘솔 로그인은 사용자가 수행(처음에 다른 프로필 창에서 로그인해 세션이 안 잡히는 문제가 있었음 → 자동화 탭과 같은 프로필에서 로그인해 해결). - 웹 터미널 진입 후 발견: 프롬프트가 admin@ip-172-26-10-166. 신형 데비안 이미지라 사용자가 bitnami가 아니라 admin이고, /opt/bitnami도 없음. 웹루트는 Apache DocumentRoot /var/www/html. - 공개키 등록(authorized_keys 2줄째), /var/www/html/podcast 생성 (admin:admin 소유). - 우분투 스크립트의 REMOTE를 bitnami→admin으로 sed 수정, 키 인증 "SSH-OK" 확인, 첫 rsync 성공. [5단계: 공개 검증과 피드 보강 (14:16~14:21)] - https://kimkj.com/podcast/*/feed.xml 200, cover 200, mp3 Range 206 확인. - content-length 1426 미스터리 → Cloudflare 전송 압축으로 판명(원본 6570). - castfeedvalidator: 기본·애플·Podcasting2.0 테스트 통과. 경고 1건(에피소드 link 없음) → kimkj_post_result.json의 게시글 URL을 link로 넣도록 make_episode_meta/build_feed 개선 + 소급 패치. - 이때 실수 1건: 수정본을 scp로 다시 보내며 우분투에서 sed로 고쳤던 REMOTE(admin)가 맥 사본(bitnami)으로 덮여 rsync가 다시 실패. → 교훈: 원본은 맥 사본( /tmp/podcast_setup → 이후 영구 보존 위치로) 한 곳에서만 고치고 서버에 밀어 넣는다. 즉시 맥 사본을 admin으로 고쳐 재전송, 해결. [6단계: 플랫폼 등록 (14:30~15:02)] - 아마존: OTP는 Gmail에서 자동으로 읽어 안내(916777)했으나, 비밀번호 찾기의 "계정 이름 확인"에서 김경진/KYUNGJIN KIM 모두 불일치, 시도 초과로 재설정 잠김 → 오늘 보류 결정(영향 최소 플랫폼). - 스포티파이: 구글 로그인 직후 대시보드 500 에러 → 로그인 페이지로 재진입하니 세션 정상(일시 장애로 판단). "기존 프로그램 찾기 → Somewhere else → 피드 URL → 이메일 코드(10170616 / 83024631, Gmail 자동 조회) → 국가·언어·카테고리 → 제출" 두 쇼 완료. 쿠키 배너가 하단 진행 버튼을 가리는 문제는 배너를 닫아 해결. - 애플: Podcasts Connect 가입(계정 이름 김경진, 개인) → 약관 동의 (마케팅 수신은 끄도록 시도) → RSS 피드 추가 두 쇼 → 피드 처리 완료 확인 → 콘텐츠 권한 "타사 콘텐츠 없음" + 연락처 저장 → 게시 시도에서 "업데이트 빈도" 필수값 누락 발견 → "매일" 선택, 저장 후 게시. 15:00 오늘 AI 뉴스, 15:02 중국 브리핑 "이용 가능". - 사후 검증: iTunes lookup API에서 두 쇼 등재 확인(에피소드 수 인덱싱 진행 중), 스포티파이 쇼 링크 200 확인. [설계상 의도된 성질 (버그 아님)] - 같은 날짜를 여러 번 publish해도 안전(멱등). "이미 게시됨"으로 통과. - 원천 오디오가 없는 날은 조용히 건너뜀. - rsync 실패는 파이프라인을 멈추지 않고 다음 회차에서 자동 복구. - 12:00 cron은 1차(10:20) 누락분의 재시도용. 16. 참고 자료 ---------------------------------------------------------------- - 사전 연구 보고서: ~/iCloud/ai_workspace/podcast_feed_automation_research_20260610.txt (플랫폼 정책 출처 URL 목록 포함) - Apple 피드 요건: https://podcasters.apple.com/support/823-podcast-requirements - 피드 검증: https://castfeedvalidator.com - 애플 등재 확인 API: https://itunes.apple.com/lookup?id=프로그램ID - Cloudflare R2 요금: https://developers.cloudflare.com/r2/pricing/ - kimkj.com DNS 이력: ~/iCloud/AI-Agent-Harness/harness/facts/kimkj_com_cloudflare_dns_migration_20260605.md (끝) [2026-06-11 음성 속도 표준 0.9506] - 두 남녀 대화(Gemini 2.5 Pro TTS)의 최종 속도 표준은 원본의 0.9506배다. (기존 0.98 운용분을 기준 100으로 보고 3% 추가 감속: 0.98 x 0.97 = 0.9506) - 한국어 쇼: 각 영상 파이프라인 .env의 TTS_SPEED=0.9506. build_tts.py가 슬라이드 wav에 ffmpeg atempo(음정 유지)로 적용한다. - 영어 쇼: publish_podcast_en.sh가 해당 쇼 한국어 .env의 TTS_SPEED를 읽어 build_tts_en.py에 넘기고, 최종 합본 mp3에 atempo를 한 번 적용한다. - 속도를 다시 바꿀 때는 Daily/China 두 .env의 TTS_SPEED만 고치면 4개 쇼 전체에 적용된다.