@yeongjaeyou/claude-code-config 0.18.6 → 0.18.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/gh/auto-review-loop.md +2 -2
- package/.claude/commands/gh/decompose-issue.md +12 -12
- package/.claude/commands/gh/resolve-and-review.md +1 -1
- package/.claude/guidelines/id-reference.md +2 -2
- package/.claude/guidelines/prd-guide.md +13 -13
- package/.claude/guidelines/work-guidelines.md +1 -1
- package/.claude/hooks/notify_osc.sh +7 -7
- package/.claude/skills/code-explorer/SKILL.md +21 -21
- package/.claude/skills/midjourney-imagineapi/SKILL.md +260 -0
- package/.claude/skills/midjourney-imagineapi/references/imagineapi-integration.md +197 -0
- package/.claude/skills/midjourney-imagineapi/references/midjourney-parameters.md +393 -0
- package/.claude/skills/midjourney-imagineapi/references/midjourney-style-guide.md +296 -0
- package/.claude/skills/midjourney-imagineapi/references/prompt-examples.md +428 -0
- package/.claude/skills/notion-md-uploader/scripts/__pycache__/markdown_parser.cpython-311.pyc +0 -0
- package/.claude/skills/notion-md-uploader/scripts/__pycache__/notion_client.cpython-311.pyc +0 -0
- package/.claude/skills/notion-md-uploader/scripts/__pycache__/notion_converter.cpython-311.pyc +0 -0
- package/.claude/skills/notion-md-uploader/scripts/__pycache__/upload_md.cpython-311.pyc +0 -0
- package/package.json +1 -1
- package/.claude/skills/youtube-collector/README.md +0 -107
- package/.claude/skills/youtube-collector/SKILL.md +0 -158
- package/.claude/skills/youtube-collector/references/data-schema.md +0 -110
- package/.claude/skills/youtube-collector/scripts/collect_videos.py +0 -304
- package/.claude/skills/youtube-collector/scripts/fetch_transcript.py +0 -138
- package/.claude/skills/youtube-collector/scripts/fetch_videos.py +0 -229
- package/.claude/skills/youtube-collector/scripts/register_channel.py +0 -247
- package/.claude/skills/youtube-collector/scripts/setup_api_key.py +0 -151
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
# YouTube Collector 데이터 스키마
|
|
2
|
-
|
|
3
|
-
## 설정 파일 구조
|
|
4
|
-
|
|
5
|
-
### API 키 (사용자 홈 디렉토리)
|
|
6
|
-
|
|
7
|
-
보안을 위해 API 키는 코드베이스 외부에 저장됨.
|
|
8
|
-
|
|
9
|
-
**경로:**
|
|
10
|
-
- macOS/Linux: `~/.config/youtube-collector/config.yaml`
|
|
11
|
-
- Windows: `%APPDATA%\youtube-collector\config.yaml`
|
|
12
|
-
|
|
13
|
-
```yaml
|
|
14
|
-
api_key: "YOUR_YOUTUBE_DATA_API_KEY"
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
**설정 명령:**
|
|
18
|
-
```bash
|
|
19
|
-
python3 scripts/setup_api_key.py
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
### 프로젝트 설정 (.reference/)
|
|
23
|
-
|
|
24
|
-
```
|
|
25
|
-
.reference/
|
|
26
|
-
├── youtube-config.yaml # 프로젝트 설정 (API 키 제외)
|
|
27
|
-
├── channels.yaml # 등록된 채널 목록
|
|
28
|
-
└── contents/
|
|
29
|
-
└── {channel_handle}/ # 채널별 폴더 (@ 제외)
|
|
30
|
-
└── {video_id}.yaml # 영상별 데이터
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## youtube-config.yaml
|
|
34
|
-
|
|
35
|
-
```yaml
|
|
36
|
-
# 자막 우선 언어 (기본: ko)
|
|
37
|
-
default_language: "ko"
|
|
38
|
-
|
|
39
|
-
# 채널당 최대 수집 개수 (기본: 10)
|
|
40
|
-
max_results: 10
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## channels.yaml
|
|
44
|
-
|
|
45
|
-
```yaml
|
|
46
|
-
channels:
|
|
47
|
-
- id: "UCxxxxxxxxxxxxxxxxxxxxxx" # 채널 ID (UC로 시작)
|
|
48
|
-
handle: "@channelname" # 채널 핸들
|
|
49
|
-
name: "채널 표시 이름" # 사람이 읽기 쉬운 이름
|
|
50
|
-
added_at: "2025-12-13" # 등록일
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## contents/{channel_handle}/{video_id}.yaml
|
|
54
|
-
|
|
55
|
-
```yaml
|
|
56
|
-
# 기본 정보
|
|
57
|
-
video_id: "abc123xyz"
|
|
58
|
-
title: "영상 제목"
|
|
59
|
-
published_at: "2025-12-10T10:00:00Z"
|
|
60
|
-
url: "https://youtube.com/watch?v=abc123xyz"
|
|
61
|
-
thumbnail: "https://i.ytimg.com/vi/abc123xyz/maxresdefault.jpg"
|
|
62
|
-
description: |
|
|
63
|
-
영상 설명 전체 텍스트
|
|
64
|
-
duration: "PT10M30S" # ISO 8601 형식
|
|
65
|
-
collected_at: "2025-12-13T15:00:00Z" # 수집 시점
|
|
66
|
-
|
|
67
|
-
# 자막 정보
|
|
68
|
-
transcript:
|
|
69
|
-
available: true # 자막 존재 여부
|
|
70
|
-
language: "ko" # 자막 언어
|
|
71
|
-
text: |
|
|
72
|
-
자막 전체 텍스트...
|
|
73
|
-
|
|
74
|
-
# 요약 정보 (초기값: null, AI가 생성하여 추가)
|
|
75
|
-
summary:
|
|
76
|
-
source: "transcript" # "transcript" 또는 "description"
|
|
77
|
-
content: |
|
|
78
|
-
## 서론
|
|
79
|
-
- 문제 제기 또는 주제 소개
|
|
80
|
-
- 영상의 목적/배경
|
|
81
|
-
|
|
82
|
-
## 본론
|
|
83
|
-
- 핵심 내용 상세 설명
|
|
84
|
-
- 해결책, 방법론, 예시 등
|
|
85
|
-
- 주요 포인트별 정리
|
|
86
|
-
|
|
87
|
-
## 결론
|
|
88
|
-
- 핵심 요약
|
|
89
|
-
- 시사점 또는 다음 단계
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
### 자막이 없는 경우
|
|
93
|
-
|
|
94
|
-
```yaml
|
|
95
|
-
transcript:
|
|
96
|
-
available: false
|
|
97
|
-
language: null
|
|
98
|
-
text: null
|
|
99
|
-
|
|
100
|
-
summary:
|
|
101
|
-
source: "description"
|
|
102
|
-
content: |
|
|
103
|
-
(설명 기반 요약 내용)
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
## 중복 방지 규칙
|
|
107
|
-
|
|
108
|
-
- 파일명이 `{video_id}.yaml` 형식
|
|
109
|
-
- 수집 전 해당 파일 존재 여부로 중복 체크
|
|
110
|
-
- 이미 존재하는 video_id는 스킵
|
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
채널의 영상을 수집하고 .reference/ 폴더에 YAML 파일로 저장하는 통합 스크립트
|
|
4
|
-
|
|
5
|
-
Usage:
|
|
6
|
-
python collect_videos.py --channel-id UC... --output-dir .reference/
|
|
7
|
-
python collect_videos.py --channel-handle @channelname --output-dir .reference/
|
|
8
|
-
python collect_videos.py --all --output-dir .reference/ # channels.yaml의 모든 채널
|
|
9
|
-
|
|
10
|
-
Output:
|
|
11
|
-
JSON 형식으로 처리 결과 요약 출력
|
|
12
|
-
|
|
13
|
-
Requirements:
|
|
14
|
-
pip install google-api-python-client youtube-transcript-api pyyaml
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
import argparse
|
|
18
|
-
import json
|
|
19
|
-
import os
|
|
20
|
-
import sys
|
|
21
|
-
from datetime import datetime
|
|
22
|
-
from pathlib import Path
|
|
23
|
-
|
|
24
|
-
# 같은 디렉토리의 모듈 import
|
|
25
|
-
script_dir = Path(__file__).parent
|
|
26
|
-
sys.path.insert(0, str(script_dir))
|
|
27
|
-
|
|
28
|
-
try:
|
|
29
|
-
from fetch_videos import (
|
|
30
|
-
load_api_key,
|
|
31
|
-
get_channel_id_from_handle,
|
|
32
|
-
fetch_videos,
|
|
33
|
-
)
|
|
34
|
-
from googleapiclient.discovery import build
|
|
35
|
-
except ImportError as e:
|
|
36
|
-
print(json.dumps({
|
|
37
|
-
"error": "필요한 모듈을 import할 수 없습니다.",
|
|
38
|
-
"detail": str(e),
|
|
39
|
-
"install": "pip install google-api-python-client pyyaml"
|
|
40
|
-
}))
|
|
41
|
-
sys.exit(1)
|
|
42
|
-
|
|
43
|
-
try:
|
|
44
|
-
from fetch_transcript import fetch_transcript
|
|
45
|
-
except ImportError as e:
|
|
46
|
-
print(json.dumps({
|
|
47
|
-
"error": "fetch_transcript 모듈을 import할 수 없습니다.",
|
|
48
|
-
"detail": str(e),
|
|
49
|
-
"install": "pip install youtube-transcript-api"
|
|
50
|
-
}))
|
|
51
|
-
sys.exit(1)
|
|
52
|
-
|
|
53
|
-
try:
|
|
54
|
-
import yaml
|
|
55
|
-
except ImportError:
|
|
56
|
-
print(json.dumps({
|
|
57
|
-
"error": "pyyaml이 설치되어 있지 않습니다.",
|
|
58
|
-
"install": "pip install pyyaml"
|
|
59
|
-
}))
|
|
60
|
-
sys.exit(1)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def load_channels(output_dir: str) -> list:
|
|
64
|
-
"""channels.yaml에서 등록된 채널 목록 로드"""
|
|
65
|
-
channels_file = Path(output_dir) / "channels.yaml"
|
|
66
|
-
|
|
67
|
-
if not channels_file.exists():
|
|
68
|
-
return []
|
|
69
|
-
|
|
70
|
-
try:
|
|
71
|
-
with open(channels_file, 'r', encoding='utf-8') as f:
|
|
72
|
-
data = yaml.safe_load(f)
|
|
73
|
-
return data.get('channels', []) if data else []
|
|
74
|
-
except Exception:
|
|
75
|
-
return []
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def get_existing_video_ids(output_dir: str, channel_handle: str) -> set:
|
|
79
|
-
"""채널 폴더에서 이미 수집된 video_id 목록 반환"""
|
|
80
|
-
# @ 제거
|
|
81
|
-
handle_clean = channel_handle.lstrip('@')
|
|
82
|
-
channel_dir = Path(output_dir) / "contents" / handle_clean
|
|
83
|
-
|
|
84
|
-
if not channel_dir.exists():
|
|
85
|
-
return set()
|
|
86
|
-
|
|
87
|
-
video_ids = set()
|
|
88
|
-
for yaml_file in channel_dir.glob("*.yaml"):
|
|
89
|
-
# 파일명이 video_id.yaml 형식
|
|
90
|
-
video_ids.add(yaml_file.stem)
|
|
91
|
-
|
|
92
|
-
return video_ids
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def save_video_yaml(output_dir: str, channel_handle: str, video_data: dict, transcript_data: dict) -> str:
|
|
96
|
-
"""영상 데이터를 YAML 파일로 저장"""
|
|
97
|
-
# @ 제거
|
|
98
|
-
handle_clean = channel_handle.lstrip('@')
|
|
99
|
-
channel_dir = Path(output_dir) / "contents" / handle_clean
|
|
100
|
-
|
|
101
|
-
# 폴더 생성
|
|
102
|
-
channel_dir.mkdir(parents=True, exist_ok=True)
|
|
103
|
-
|
|
104
|
-
# YAML 데이터 구성
|
|
105
|
-
yaml_data = {
|
|
106
|
-
'video_id': video_data['video_id'],
|
|
107
|
-
'title': video_data['title'],
|
|
108
|
-
'published_at': video_data['published_at'],
|
|
109
|
-
'url': video_data['url'],
|
|
110
|
-
'thumbnail': video_data['thumbnail'],
|
|
111
|
-
'description': video_data['description'],
|
|
112
|
-
'duration': video_data['duration'],
|
|
113
|
-
'collected_at': datetime.utcnow().isoformat() + "Z",
|
|
114
|
-
'transcript': {
|
|
115
|
-
'available': transcript_data['available'],
|
|
116
|
-
'language': transcript_data.get('language'),
|
|
117
|
-
'text': transcript_data.get('text')
|
|
118
|
-
},
|
|
119
|
-
'summary': None # AI가 나중에 추가
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
# 파일 저장
|
|
123
|
-
file_path = channel_dir / f"{video_data['video_id']}.yaml"
|
|
124
|
-
|
|
125
|
-
with open(file_path, 'w', encoding='utf-8') as f:
|
|
126
|
-
yaml.dump(yaml_data, f, allow_unicode=True, default_flow_style=False, sort_keys=False)
|
|
127
|
-
|
|
128
|
-
return str(file_path)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def collect_channel_videos(
|
|
132
|
-
youtube,
|
|
133
|
-
channel_id: str,
|
|
134
|
-
channel_handle: str,
|
|
135
|
-
output_dir: str,
|
|
136
|
-
max_results: int = 10,
|
|
137
|
-
language: str = 'ko',
|
|
138
|
-
skip_existing: bool = True
|
|
139
|
-
) -> dict:
|
|
140
|
-
"""단일 채널의 영상을 수집하고 저장"""
|
|
141
|
-
result = {
|
|
142
|
-
'channel': channel_handle,
|
|
143
|
-
'channel_id': channel_id,
|
|
144
|
-
'new_videos': [],
|
|
145
|
-
'skipped': 0,
|
|
146
|
-
'errors': []
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
# 기존 video_id 목록
|
|
150
|
-
existing_ids = get_existing_video_ids(output_dir, channel_handle) if skip_existing else set()
|
|
151
|
-
|
|
152
|
-
# 영상 목록 조회
|
|
153
|
-
videos = fetch_videos(youtube, channel_id, max_results)
|
|
154
|
-
|
|
155
|
-
for video in videos:
|
|
156
|
-
video_id = video['video_id']
|
|
157
|
-
|
|
158
|
-
# 중복 체크
|
|
159
|
-
if video_id in existing_ids:
|
|
160
|
-
result['skipped'] += 1
|
|
161
|
-
continue
|
|
162
|
-
|
|
163
|
-
try:
|
|
164
|
-
# 자막 수집
|
|
165
|
-
transcript = fetch_transcript(video_id, language)
|
|
166
|
-
|
|
167
|
-
# YAML 저장
|
|
168
|
-
file_path = save_video_yaml(output_dir, channel_handle, video, transcript)
|
|
169
|
-
|
|
170
|
-
result['new_videos'].append({
|
|
171
|
-
'video_id': video_id,
|
|
172
|
-
'title': video['title'],
|
|
173
|
-
'file_path': file_path,
|
|
174
|
-
'transcript_available': transcript['available']
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
except Exception as e:
|
|
178
|
-
result['errors'].append({
|
|
179
|
-
'video_id': video_id,
|
|
180
|
-
'error': str(e)
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
return result
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def main():
|
|
187
|
-
parser = argparse.ArgumentParser(description='YouTube 채널 영상 수집 및 저장')
|
|
188
|
-
parser.add_argument('--channel-id', help='채널 ID (UC...)')
|
|
189
|
-
parser.add_argument('--channel-handle', help='채널 핸들 (@username)')
|
|
190
|
-
parser.add_argument('--all', action='store_true', help='channels.yaml의 모든 채널 처리')
|
|
191
|
-
parser.add_argument('--output-dir', default='.reference', help='저장 디렉토리 (기본: .reference)')
|
|
192
|
-
parser.add_argument('--max-results', type=int, default=10, help='채널당 최대 수집 개수 (기본: 10)')
|
|
193
|
-
parser.add_argument('--language', default='ko', help='자막 우선 언어 (기본: ko)')
|
|
194
|
-
parser.add_argument('--no-skip-existing', action='store_true', help='기존 파일도 덮어쓰기')
|
|
195
|
-
parser.add_argument('--api-key', help='YouTube Data API 키 (미지정시 설정 파일에서 로드)')
|
|
196
|
-
|
|
197
|
-
args = parser.parse_args()
|
|
198
|
-
|
|
199
|
-
# 옵션 검증
|
|
200
|
-
if not args.all and not args.channel_id and not args.channel_handle:
|
|
201
|
-
print(json.dumps({
|
|
202
|
-
"error": "--all, --channel-id, --channel-handle 중 하나를 지정해야 합니다."
|
|
203
|
-
}))
|
|
204
|
-
sys.exit(1)
|
|
205
|
-
|
|
206
|
-
# API 키 로드
|
|
207
|
-
api_key = args.api_key or load_api_key()
|
|
208
|
-
if not api_key:
|
|
209
|
-
print(json.dumps({
|
|
210
|
-
"error": "YouTube Data API 키가 설정되지 않았습니다.",
|
|
211
|
-
"help": "python3 scripts/setup_api_key.py로 설정해주세요."
|
|
212
|
-
}))
|
|
213
|
-
sys.exit(1)
|
|
214
|
-
|
|
215
|
-
# YouTube API 초기화
|
|
216
|
-
try:
|
|
217
|
-
youtube = build('youtube', 'v3', developerKey=api_key)
|
|
218
|
-
except Exception as e:
|
|
219
|
-
print(json.dumps({
|
|
220
|
-
"error": "YouTube API 초기화 실패",
|
|
221
|
-
"message": str(e)
|
|
222
|
-
}))
|
|
223
|
-
sys.exit(1)
|
|
224
|
-
|
|
225
|
-
# 처리할 채널 목록 결정
|
|
226
|
-
channels_to_process = []
|
|
227
|
-
|
|
228
|
-
if args.all:
|
|
229
|
-
# channels.yaml에서 모든 채널 로드
|
|
230
|
-
channels = load_channels(args.output_dir)
|
|
231
|
-
if not channels:
|
|
232
|
-
print(json.dumps({
|
|
233
|
-
"error": "등록된 채널이 없습니다.",
|
|
234
|
-
"help": "먼저 register_channel.py로 채널을 등록해주세요."
|
|
235
|
-
}))
|
|
236
|
-
sys.exit(1)
|
|
237
|
-
|
|
238
|
-
for ch in channels:
|
|
239
|
-
channels_to_process.append({
|
|
240
|
-
'id': ch.get('id'),
|
|
241
|
-
'handle': ch.get('handle', '')
|
|
242
|
-
})
|
|
243
|
-
else:
|
|
244
|
-
# 단일 채널 처리
|
|
245
|
-
channel_id = args.channel_id
|
|
246
|
-
channel_handle = args.channel_handle or ''
|
|
247
|
-
|
|
248
|
-
if not channel_id and channel_handle:
|
|
249
|
-
channel_id = get_channel_id_from_handle(youtube, channel_handle)
|
|
250
|
-
if not channel_id:
|
|
251
|
-
print(json.dumps({
|
|
252
|
-
"error": f"채널을 찾을 수 없습니다: {channel_handle}"
|
|
253
|
-
}))
|
|
254
|
-
sys.exit(1)
|
|
255
|
-
|
|
256
|
-
channels_to_process.append({
|
|
257
|
-
'id': channel_id,
|
|
258
|
-
'handle': channel_handle
|
|
259
|
-
})
|
|
260
|
-
|
|
261
|
-
# 결과 수집
|
|
262
|
-
all_results = {
|
|
263
|
-
'processed_at': datetime.utcnow().isoformat() + "Z",
|
|
264
|
-
'output_dir': args.output_dir,
|
|
265
|
-
'channels_processed': 0,
|
|
266
|
-
'total_new_videos': 0,
|
|
267
|
-
'total_skipped': 0,
|
|
268
|
-
'results': [],
|
|
269
|
-
'errors': []
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
skip_existing = not args.no_skip_existing
|
|
273
|
-
|
|
274
|
-
for channel in channels_to_process:
|
|
275
|
-
try:
|
|
276
|
-
result = collect_channel_videos(
|
|
277
|
-
youtube=youtube,
|
|
278
|
-
channel_id=channel['id'],
|
|
279
|
-
channel_handle=channel['handle'],
|
|
280
|
-
output_dir=args.output_dir,
|
|
281
|
-
max_results=args.max_results,
|
|
282
|
-
language=args.language,
|
|
283
|
-
skip_existing=skip_existing
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
all_results['channels_processed'] += 1
|
|
287
|
-
all_results['total_new_videos'] += len(result['new_videos'])
|
|
288
|
-
all_results['total_skipped'] += result['skipped']
|
|
289
|
-
all_results['results'].append(result)
|
|
290
|
-
|
|
291
|
-
if result['errors']:
|
|
292
|
-
all_results['errors'].extend(result['errors'])
|
|
293
|
-
|
|
294
|
-
except Exception as e:
|
|
295
|
-
all_results['errors'].append({
|
|
296
|
-
'channel': channel.get('handle') or channel.get('id'),
|
|
297
|
-
'error': str(e)
|
|
298
|
-
})
|
|
299
|
-
|
|
300
|
-
print(json.dumps(all_results, ensure_ascii=False, indent=2))
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if __name__ == "__main__":
|
|
304
|
-
main()
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
YouTube 영상의 자막(transcript)을 가져오는 스크립트
|
|
4
|
-
|
|
5
|
-
Usage:
|
|
6
|
-
python fetch_transcript.py --video-id VIDEO_ID [--language ko]
|
|
7
|
-
|
|
8
|
-
Output:
|
|
9
|
-
JSON 형식으로 자막 출력
|
|
10
|
-
|
|
11
|
-
Requirements:
|
|
12
|
-
pip install youtube-transcript-api
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
import argparse
|
|
16
|
-
import json
|
|
17
|
-
import sys
|
|
18
|
-
|
|
19
|
-
try:
|
|
20
|
-
from youtube_transcript_api import YouTubeTranscriptApi
|
|
21
|
-
from youtube_transcript_api._errors import (
|
|
22
|
-
TranscriptsDisabled,
|
|
23
|
-
NoTranscriptFound,
|
|
24
|
-
VideoUnavailable,
|
|
25
|
-
CouldNotRetrieveTranscript
|
|
26
|
-
)
|
|
27
|
-
except ImportError:
|
|
28
|
-
print(json.dumps({
|
|
29
|
-
"error": "youtube-transcript-api가 설치되어 있지 않습니다.",
|
|
30
|
-
"install": "pip install youtube-transcript-api"
|
|
31
|
-
}))
|
|
32
|
-
sys.exit(1)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def fetch_transcript(video_id: str, preferred_language: str = 'ko') -> dict:
|
|
36
|
-
"""
|
|
37
|
-
영상 자막 가져오기
|
|
38
|
-
|
|
39
|
-
Args:
|
|
40
|
-
video_id: YouTube 영상 ID
|
|
41
|
-
preferred_language: 우선 언어 코드 (기본: ko)
|
|
42
|
-
|
|
43
|
-
Returns:
|
|
44
|
-
dict: {
|
|
45
|
-
'available': bool,
|
|
46
|
-
'language': str or None,
|
|
47
|
-
'text': str or None,
|
|
48
|
-
'segments': list or None,
|
|
49
|
-
'error': str or None
|
|
50
|
-
}
|
|
51
|
-
"""
|
|
52
|
-
result = {
|
|
53
|
-
'available': False,
|
|
54
|
-
'language': None,
|
|
55
|
-
'text': None,
|
|
56
|
-
'segments': None,
|
|
57
|
-
'error': None
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
try:
|
|
61
|
-
# 사용 가능한 자막 목록 조회
|
|
62
|
-
transcript_list = YouTubeTranscriptApi.list_transcripts(video_id)
|
|
63
|
-
|
|
64
|
-
transcript = None
|
|
65
|
-
|
|
66
|
-
# 1. 우선 언어의 수동 자막 시도
|
|
67
|
-
try:
|
|
68
|
-
transcript = transcript_list.find_manually_created_transcript([preferred_language])
|
|
69
|
-
except NoTranscriptFound:
|
|
70
|
-
pass
|
|
71
|
-
|
|
72
|
-
# 2. 우선 언어의 자동 생성 자막 시도
|
|
73
|
-
if not transcript:
|
|
74
|
-
try:
|
|
75
|
-
transcript = transcript_list.find_generated_transcript([preferred_language])
|
|
76
|
-
except NoTranscriptFound:
|
|
77
|
-
pass
|
|
78
|
-
|
|
79
|
-
# 3. 영어 자막 시도 (한국어 없을 경우)
|
|
80
|
-
if not transcript and preferred_language != 'en':
|
|
81
|
-
try:
|
|
82
|
-
transcript = transcript_list.find_manually_created_transcript(['en'])
|
|
83
|
-
except NoTranscriptFound:
|
|
84
|
-
try:
|
|
85
|
-
transcript = transcript_list.find_generated_transcript(['en'])
|
|
86
|
-
except NoTranscriptFound:
|
|
87
|
-
pass
|
|
88
|
-
|
|
89
|
-
# 4. 아무 자막이나 가져오기
|
|
90
|
-
if not transcript:
|
|
91
|
-
try:
|
|
92
|
-
for t in transcript_list:
|
|
93
|
-
transcript = t
|
|
94
|
-
break
|
|
95
|
-
except:
|
|
96
|
-
pass
|
|
97
|
-
|
|
98
|
-
if transcript:
|
|
99
|
-
segments = transcript.fetch()
|
|
100
|
-
full_text = ' '.join([segment['text'] for segment in segments])
|
|
101
|
-
|
|
102
|
-
result['available'] = True
|
|
103
|
-
result['language'] = transcript.language_code
|
|
104
|
-
result['text'] = full_text
|
|
105
|
-
result['segments'] = segments
|
|
106
|
-
|
|
107
|
-
except TranscriptsDisabled:
|
|
108
|
-
result['error'] = "이 영상은 자막이 비활성화되어 있습니다."
|
|
109
|
-
except CouldNotRetrieveTranscript:
|
|
110
|
-
result['error'] = "이 영상에는 사용 가능한 자막이 없습니다."
|
|
111
|
-
except VideoUnavailable:
|
|
112
|
-
result['error'] = "영상을 찾을 수 없습니다."
|
|
113
|
-
except Exception as e:
|
|
114
|
-
result['error'] = str(e)
|
|
115
|
-
|
|
116
|
-
return result
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def main():
|
|
120
|
-
parser = argparse.ArgumentParser(description='YouTube 영상 자막 가져오기')
|
|
121
|
-
parser.add_argument('--video-id', required=True, help='영상 ID')
|
|
122
|
-
parser.add_argument('--language', default='ko', help='우선 언어 (기본: ko)')
|
|
123
|
-
parser.add_argument('--include-segments', action='store_true',
|
|
124
|
-
help='타임스탬프가 포함된 세그먼트 정보 포함')
|
|
125
|
-
|
|
126
|
-
args = parser.parse_args()
|
|
127
|
-
|
|
128
|
-
result = fetch_transcript(args.video_id, args.language)
|
|
129
|
-
|
|
130
|
-
# 세그먼트 정보 제외 옵션
|
|
131
|
-
if not args.include_segments and result.get('segments'):
|
|
132
|
-
del result['segments']
|
|
133
|
-
|
|
134
|
-
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if __name__ == "__main__":
|
|
138
|
-
main()
|