Communitygithub.com

podcast-digest

일본어 팟캐스트 에피소드나 녹화된 2인 영상 통화를 수직 9:16 숏츠(최대 180초)로 변환합니다. 일본어 자막이 영상에 포함되며, 사용자가 지정한 하이라이트를 연결해 하나의 클립으로 만듭니다.

podcast-digest란 무엇인가요?

이 스킬은 팟캐스트 오디오나 StreamYard 같은 2인 영상 녹화를 입력받아, 1080×1920 해상도의 수직 숏츠 영상(최대 180초)을 생성합니다. 주요 특징은 사람이 직접 **자막 교정**(1b)과 **하이라이트 선택**(2)이라는 두 가지 판단 단계를 수행한다는 점입니다. 나머지(오디오 추출, 음성 인식, 타이밍 계산, 렌더링)는 스크립트가 자동으로 처리합니다. 에피소드별로 디렉토리가 생성되며, OpenAI Whisper-1로 일본어 전사를 수행하고, 사용자가 corrections.md를 통해 잘못 인식된 단어를 수정합니다. 그 후 사용자가 clip.draft.json에 하이라이트 구간을 정의하면, build_clip.py가 이를 조정하고 render.py가 최종 자막 포함 영상을 출력합니다. 렌더링 전 두 번의 인간 체크포인트가 있어 품질을 보장합니다. 이 스킬은 Claude Code, Cursor, Codex 같은 AI 에이전트에서 사용하도록 설계되었으며, "다이제스트", "하이라이트", "숏츠”, “릴스”, “클립”, “切り抜き動画" 제작 요청에 적합합니다.

지원 대상✓Claude Code✓Codex CLI~Cursor
npx skills add https://github.com/tomoima525/podcast-digest-skill/tree/HEAD

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

팟캐스트 / 녹화 → 하이라이트 영상

확정적인 작업(오디오, API 호출, 타이밍 계산, 렌더링)은 스크립트가 처리합니다. 당신은 두 가지 판단 단계를 수행합니다: 자막 교정(1b)과 하이라이트 선택(2). 두 번의 인간 체크포인트가 있습니다. 절대 건너뛰지 마세요.

SKILL_DIR은 이 파일이 포함된 디렉토리를 의미합니다. 스크립트는 uv로 실행하며, 이 스킬의 잠긴 환경을 사용합니다. uv run --project "$SKILL_DIR" "$SKILL_DIR/scripts/<name>.py" <episode_dir>. 아래 단계에서 <name>.py <ep>는 위 명령어를 줄여 쓴 것입니다. 모든 스크립트는 생성하거나 건너뛴 내용을 출력합니다. 재실행은 항상 안전합니다: 완료된 단계는 건너뛰고, 입력이 변경되면 파생 파일이 다시 빌드됩니다. --force를 사용하면 단계를 다시 실행합니다.

에피소드 레이아웃 (에피소드당 하나의 디렉토리)

episodes/<slug>/
  input.mp4                        사용자가 제공 (오디오 팟캐스트 또는 영상 녹화)
  shownote.txt                     사용자가 제공 (권장: 이름, 제품, 주제)
  episode.json                     영상 녹화의 경우 당신이 작성: {"layout": "stacked"}
  audio.mp3, transcript.raw.json   1a단계 (transcribe.py)
  corrections.md                   1b단계 (당신이 작성)
  transcript.json                  1b단계 (apply_corrections.py)
  clip.draft.json                  2단계   (당신이 작성)
  clip.json                        2단계   (build_clip.py)
  layout.preview.png               2단계   (preview_layout.py, 영상 녹화 전용)
  digest.ass, digest.mp4           3단계   (render.py)

두 종류의 에피소드, 하나의 파이프라인

오디오 팟캐스트영상 녹화 (StreamYard 2분할)
episode.json없음 (또는 {"layout": "background"}){"layout": "stacked"}
화면assets/background.png왼쪽 화자가 위, 오른쪽 화자가 아래, 사운드와 동기화된 자막이 사이 띠에 표시
사운드audio.mp3input.mp4에서 추출, 화면과 동기화

그 외 모든 것(전사, 교정, 하이라이트 선택, 자막, 음량)은 동일합니다. 50분, 약 1.5GB 녹화도 문제없습니다. 오디오만 업로드되며, 렌더링은 각 하이라이트 위치로 바로 이동합니다.

0단계: 설정

  1. check_env.py를 실행합니다 (에피소드 인자 없음). ffmpeg/libass, 배경 이미지, 폰트, 그리고 OPENAI_API_KEY가 셸 또는 $SKILL_DIR/.env에 설정되어 있는지 확인합니다. 종료 코드 1이면 멈추고 사용자에게 README → Setup을 안내합니다.
    • .env 파일을 절대 읽거나, 출력하거나, cat 또는 grep하지 마세요. 사용자에게 키를 채팅에 붙여넣도록 요청하지 마세요. 사용자가 직접 .env에 키를 넣습니다: cp .env.example .env 후 파일을 편집합니다.
  2. episodes/<slug>/ 디렉토리를 만듭니다 (짧은 ASCII 슬러그, 예: ep042). 녹화 파일을 input.mp4로, 쇼노트를 shownote.txt로 복사합니다. 쇼노트가 없으면 사용자에게 출연자와 주요 이름/용어를 물어 직접 shownote.txt에 작성합니다. 경로를 제공받지 않은 녹화 파일을 이동하거나 복사하지 마세요.
  3. 두 사람이 나란히 있는 영상 녹화라면 episode.json을 작성합니다: {"layout": "stacked"}.
  4. run_all.py episodes/<slug>를 실행합니다. 1a단계를 실행하고 1b단계에서 종료 코드 10으로 멈춥니다. 이 정지는 정상입니다. 종료 코드 1은 오류를 의미합니다: 내용을 읽으세요.

1a단계: 전사 (스크립트)

run_all.py가 이미 transcribe.py를 실행했습니다. 모노 16kHz 오디오를 추출하고, 단어 타임스탬프, language="ja", 그리고 쇼노트로 만든 어휘 프롬프트를 사용하여 whisper-1을 호출합니다. 긴 파일은 자동으로 겹치는 청크로 분할됩니다. whisper-1은 때때로 음성 구간을 건너뜁니다: 인식된 단어 사이의 긴 정지 구간에 여전히 소리가 있으면 해당 구간만 다시 보내 복구된 세그먼트를 삽입합니다 (gap ... recovered 줄). 프롬프트를 보려면 transcribe.py <ep> --print-prompt를 실행합니다. 프롬프트를 재정의하려면 <ep>/prompt.txt에 텍스트를 넣고 --force로 다시 실행합니다. 재실행은 API 비용이 발생하므로 먼저 사용자에게 물어보세요.

1b단계: 자막 교정 (당신)

목표: 각 세그먼트의 text에서 잘못 인식된 단어를 수정합니다. shownote.txt를 이름, 제품, 용어의 출처로 사용합니다.

  1. shownote.txt 전체를 읽습니다. 거기에 나온 이름(호스트, 게스트), 제품, 회사, 기술 용어를 목록으로 만듭니다.

  2. 전사를 읽습니다: show_transcript.py <ep> --raw. 긴 에피소드의 경우 범위로 읽습니다 (--from-id/--to-id). 각 용어의 오인 가능성이 높은 부분을 grep합니다 (--grep クロード --grep claude). Whisper는 일반적으로 영어 이름을 가타카나로 쓰고, 철자를 틀리거나 (Wisper), 글자를 빼먹거나 (NTHROPIC), 잘못된 한자 동음이의어를 선택합니다 (制度→精度).

  3. corrections.md를 작성합니다: 마크다운 테이블 하나. 각 행은 나열된 세그먼트에서 정확한 부분 문자열을 대체합니다:

    | id | before | after | reason |
    |----|--------|-------|--------|
    | 6 | クロットコード | Claude Code | product name in shownote |
    | 7, 8, 12 | エーデント | エージェント | misrecognition of エージェント |
    
    • id: 세그먼트 ID, 쉼표로 구분, 범위 허용 (10-12). before는 해당 세그먼트의 현재 텍스트에 반드시 존재해야 합니다. 행은 순서대로 적용됩니다.
    • 인식 오류만 수정합니다. 일본어를 바꾸거나, 요약하거나, 필러를 제거하거나, "개선"하지 마세요. 문맥에서 명백한 동음이의어는 수정하고, 쇼노트가 확실하지 않을 때는 reason에 "(context)"라고 표시하세요.
    • 세그먼트가 whisper의 환각인 경우 (예: 무음 구간에「ご視聴ありがとうございました」), 비워 둡니다: | 41 | ご視聴ありがとうございました | | hallucination |.
    • 절대 세그먼트를 병합, 분할, 재정렬하거나 시간을 변경하지 마세요. 스크립트가 corrections.md를 통하지 않으면 이를 불가능하게 만듭니다. transcript.json을 수동으로 편집하지 마세요.
    • 수정할 것이 없으면 헤더 행만 작성합니다.
  4. apply_corrections.py <ep>를 실행합니다. transcript.json을 쓰고 validate_transcript.py를 실행합니다. before를 찾을 수 없으면 해당 행을 수정하고 다시 실행합니다.

  5. 체크포인트 1. 사용자에게 교정 테이블 (또는 요약, 그리고 "(context)"로 표시된 행)을 보여주고 corrections.md를 검토하도록 요청합니다. corrections.md를 편집하고 apply_corrections.py를 다시 실행하여 변경 사항을 적용합니다. 사용자가 승인한 후에만 계속 진행합니다.

2단계: 하이라이트 선택 (당신)

이 단계는 사용자의 지시가 필요합니다: 주제 (「料金の話をしているところ」) 또는 최고 부분 요청 (「大事なポイントをまとめて」). 사용자가 지시를 주지 않았으면 물어보세요. 주제를 추측하지 마세요.

숏츠는 하나 이상의 스팬(에피소드의 연속된 부분)으로 구성되며, 시간 순서대로 재생되고 하드 컷으로 연결됩니다. 총합: 최대 180초.

  1. 검색: show_transcript.py <ep> --grep <keyword> ... -C 5를 지시의 키워드와 동의어, 예상되는 철자법으로 실행합니다. 그런 다음 각 히트 주변을 넉넉히 읽어 (--from-id/--to-id) 각 논의의 실제 시작과 끝을 파악합니다. "중요한 포인트"의 경우 먼저 전체 전사를 범위로 읽고 선택 전에 후보 순간들을 ID와 함께 나열합니다.

  2. 스팬을 선택합니다:

    • 하나의 스팬: 지시가 하나의 주제를 언급하고 그 논의가 180초 안에 들어갈 때.
    • 여러 스팬 (보통 2-4개): 지시가 하이라이트를 요청하거나, 하나의 주제가 분산되어 있고 그 사이의 이야기가 주제와 무관할 때. 각 스팬은 단독으로 이해될 수 있어야 하고 이전 스팬 직후에도 말이 되어야 합니다: 하드 컷은 맥락을 제공하지 않습니다.
    • 모든 스팬에 대해: 시작은 포인트가 소개되는 곳에서, 생각 중간이나 역참조(「さっきの」「前回」「それで」)에서 시작하지 마세요. 앞의 필러나 전환 (「では本題です。」「で、」)은 start_at으로 잘라냅니다. 끝은 완전한 문장에서; end_after를 사용하여 세그먼트 중간에서 멈춥니다.
    • 많은 조각보다는 적고 완전한 스팬을 선호합니다; 약 8초 미만의 스팬은 단독으로 서기 어렵습니다.
    • 자료가 180초보다 길면 가장 강력한 부분을 유지하고 reason에 그렇게 명시합니다.
    • 최대 2개의 alternatives (다른 강력한 스팬)를 이유와 함께 추가합니다.
    • 일치하는 것이 없으면 중단하고, 사용자에게 검색한 내용과 에피소드가 다루는 내용을 알립니다. 초안을 작성하지 마세요.
  3. clip.draft.json을 작성합니다. 세그먼트 ID는 당신이 선택합니다. 스크립트가 타이밍을 계산합니다:

    {
      "instruction": "<사용자의 지시, 그대로>",
      "title": "스킬 기능과 자막의 요점",
      "reason": "이 부분들을 전체적으로 선택한 이유.",
      "spans": [
        { "start_id": 6, "end_id": 8, "start_at": "오늘은, Claude Code의 스킬 기능에 대해",
          "title": "스킬 기능이란", "reason": "주제를 소개합니다." },
        { "start_id": 12, "end_id": 12, "title": "분담이 중요", "reason": "핵심 포인트입니다." }
      ],
      "alternatives": [
        { "start_id": 120, "end_id": 131, "title": "...", "reason": "..." }
      ]
    }
    

    start_at과 end_after는 스팬의 첫 번째 및 마지막 세그먼트 텍스트의 정확한 부분 문자열이며 선택 사항입니다. 대안에도 사용할 수 있습니다. 여러 스팬이 있을 때 모든 스팬에는 reason이 필요합니다. (단일 스팬 초안은 start_id/end_id/...를 최상위에 넣을 수도 있습니다.)

  4. build_clip.py <ep>를 실행합니다. 모든 경계를 단어 타임스탬프에 맞추고, 약 0.3초의 패딩을 추가하며 (이웃 단어를 넘지 않음), 스팬을 정렬하고, 겹치지 않고 총합 ≤ 180초인지 확인한 후 clip.json을 쓰고 검증합니다. 각 스팬을 시간 범위와 길이와 함께 나열합니다. 출력된 warn 줄을 읽으세요. 인접한 스팬 (세그먼트 ID가 닿는 경우)은 겹치는 것으로 간주됩니다: 하나의 스팬으로 병합하세요.

  5. 영상 녹화 전용: preview_layout.py <ep>를 실행하고 layout.preview.png (그리고 layout.source.png)를 확인합니다. 각 패널은 한 사람을 보여주고, 중앙에 위치하며, 프레임 가장자리나 이웃 타일이 없어야 합니다. 그렇지 않으면 episode.json에 "tiles"를 설정하고 (소스 프레임의 분수) 재실행합니다:

    {"layout": "stacked",
     "tiles": [{"x": 0.0, "y": 0.14, "w": 0.497, "h": 0.70},
               {"x": 0.504, "y": 0.14, "w": 0.496, "h": 0.70}]}
    

    첫 번째 타일이 위에 갑니다. 두 항목을 바꾸면 화자가 바뀝니다. 같은 스튜디오 설정의 녹화는 동일한 타일을 공유하므로 작동하는 episode.json을 재사용하세요.

  6. 체크포인트 2. 사용자에게 계획을 보여줍니다: 전체 제목과 총 길이, 그 다음 각 스팬을 순서대로 제목, mm:ss–mm:ss, 길이, 이유, 첫 번째와 마지막 문장 (show_transcript.py <ep> --from-id A --to-id B), 그리고 대안. 영상의 경우 layout.preview.png도 보여줍니다. 사용자에게 스팬을 승인, 교체, 추가 또는 삭제하도록 요청합니다.

    • build_clip.py <ep> --promote N: 단일 스팬의 경우 대안 N을 교체; 여러 스팬의 경우 대안 N을 추가 스팬으로 추가합니다 (180초 총합 주의).
    • 다른 변경 사항: clip.draft.json을 편집하고 build_clip.py를 다시 실행합니다.
    • 사용자가 승인한 후에만 계속 진행합니다.

3단계: 렌더링 (스크립트)

run_all.py <ep> --render (또는 render.py <ep>)를 실행합니다. 출력: digest.mp4, 1080x1920, 30fps, H.264 + AAC, -14 LUFS로 정규화, digest.ass에서 자막이 포함됩니다. 스팬은 하드 컷으로 연결됩니다. 각 컷에는 50ms 오디오 페이드가 적용되어 클릭 노이즈를 방지합니다.

그런 다음 결과를 확인합니다:

  • ffprobe -v error -show_entries format=duration -of csv=p=0 <ep>/digest.mp4의 길이가 build_clip.py가 출력한 총 길이와 일치해야 합니다.
  • 각 스팬에서 프레임 하나를 추출하여 자막이 렌더링되고, 토후 상자가 아니며, (영상의 경우) 패널 프레임이 올바른지 확인합니다: ffmpeg -y -v error -ss 5 -i <ep>/digest.mp4 -frames:v 1 /tmp/frame.png.
  • 렌더링 시 폰트 Noto Sans JP를 찾을 수 없다고 경고하면 사용자에게 알립니다 (README → Setup).

digest.mp4의 경로, 제목, 각 스팬의 소스 시간 범위를 보고합니다.

문제 해결

증상해결책
OPENAI_API_KEY is not set사용자가 $SKILL_DIR/.env (템플릿 .env.example)에 추가하거나 내보냅니다. 키를 직접 처리하지 마세요
before ... not foundbefore는 해당 세그먼트의 현재 텍스트의 정확한 부분 문자열이어야 합니다 (이전 행이 변경했을 수 있음)
segments changed but not listed변경된 모든 세그먼트 ID가 corrections.md 행에 나타나야 합니다
spans total ... > 180s / exceeds 180s스팬을 삭제하거나, 더 짧게 선택하거나, start_at / end_after로 자릅니다
갭 복구 이전에 만들어진 전사에서 음성이 누락됨 (사람이 말하는 구간의 긴 점프)transcribe.py <ep> --fill-gaps는 갭만 다시 보냅니다. ID가 어떻게 이동하는지 출력합니다: corrections.md / clip.draft.json의 ID를 다시 맞추고, apply_corrections.py <ep>를 실행합니다. 복구된「ご視聴ありがとうございました」는 환각으로 비워 둡니다
같은 에피소드에서 두 번째 숏츠완료된 digest.mp4, digest.ass, clip.json, clip.draft.json의 이름을 바꾸고 (예: digest-keypoints.mp4), 새 clip.draft.json을 작성합니다
spans overlap두 스팬이 세그먼트를 공유하거나 닿습니다: 하나의 스팬으로 병합합니다
opens with 'さっき' (경고)더 나중에 시작하거나 start_at으로 잘라 스팬이 독립적으로 서도록 합니다
패널이 잘못된 영역 또는 경계선을 표시함episode.json에서 tiles를 수정하고, preview_layout.py로 확인한 후 render.py <ep>을 실행합니다
layout 'stacked' but input.mp4 has no video stream오디오 팟캐스트입니다: episode.json을 제거합니다
대체 또는 얇은 폰트로 자막 표시assets/fonts/NotoSansJP-Bold.ttf가 존재해야 합니다 (정적 Bold 파일, 가변 폰트 아님); README → Setup 참조
background.png / 폰트 설정 변경됨render.py <ep>가 감지하고 다시 렌더링합니다; 확실하려면 --force 사용

관련 스킬