================================================================
팟캐스트 자동 배포 시스템 구축 문서 (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}episodicfalse
"""
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개 쇼 전체에 적용된다.