ytcollector 1.0.8__py3-none-any.whl → 1.0.9__py3-none-any.whl
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.
- ytcollector/__init__.py +36 -11
- ytcollector/analyzer.py +205 -0
- ytcollector/cli.py +186 -218
- ytcollector/config.py +66 -62
- ytcollector/dataset_builder.py +136 -0
- ytcollector/downloader.py +328 -480
- ytcollector-1.0.9.dist-info/METADATA +207 -0
- ytcollector-1.0.9.dist-info/RECORD +11 -0
- ytcollector-1.0.9.dist-info/entry_points.txt +4 -0
- {ytcollector-1.0.8.dist-info → ytcollector-1.0.9.dist-info}/top_level.txt +0 -1
- config/settings.py +0 -39
- ytcollector/utils.py +0 -144
- ytcollector/verifier.py +0 -187
- ytcollector-1.0.8.dist-info/METADATA +0 -105
- ytcollector-1.0.8.dist-info/RECORD +0 -12
- ytcollector-1.0.8.dist-info/entry_points.txt +0 -2
- {ytcollector-1.0.8.dist-info → ytcollector-1.0.9.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ytcollector
|
|
3
|
+
Version: 1.0.9
|
|
4
|
+
Summary: YouTube 콘텐츠 수집기 - 얼굴, 번호판, 타투, 텍스트 감지
|
|
5
|
+
Author: YTCollector Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/yourusername/ytcollector
|
|
8
|
+
Project-URL: Documentation, https://github.com/yourusername/ytcollector#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/yourusername/ytcollector
|
|
10
|
+
Keywords: youtube,downloader,video-analysis,face-detection,ocr
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: yt-dlp>=2024.0.0
|
|
23
|
+
Provides-Extra: analysis
|
|
24
|
+
Requires-Dist: opencv-python>=4.5.0; extra == "analysis"
|
|
25
|
+
Requires-Dist: easyocr>=1.6.0; extra == "analysis"
|
|
26
|
+
Requires-Dist: numpy>=1.20.0; extra == "analysis"
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
29
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
31
|
+
Provides-Extra: all
|
|
32
|
+
Requires-Dist: ytcollector[analysis,dev]; extra == "all"
|
|
33
|
+
|
|
34
|
+
# YouTube 콘텐츠 수집기
|
|
35
|
+
|
|
36
|
+
유튜브에서 특정 카테고리(얼굴, 번호판, 타투, 텍스트)의 영상을 자동으로 검색, 다운로드, 분석하여 수집하는 CLI 도구입니다.
|
|
37
|
+
|
|
38
|
+
## 설치
|
|
39
|
+
|
|
40
|
+
### 필수 패키지
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install yt-dlp
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 분석 기능용 패키지 (권장)
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install opencv-python easyocr numpy
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 사용법
|
|
53
|
+
|
|
54
|
+
### 기본 실행
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
python main.py
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
기본값: 얼굴 카테고리 5개, 최대 3분 영상
|
|
61
|
+
|
|
62
|
+
### 옵션
|
|
63
|
+
|
|
64
|
+
| 옵션 | 설명 | 기본값 |
|
|
65
|
+
|------|------|--------|
|
|
66
|
+
| `-c`, `--categories` | 수집할 카테고리 | `face` |
|
|
67
|
+
| `-n`, `--count` | 카테고리당 다운로드 수 | `5` |
|
|
68
|
+
| `-d`, `--duration` | 최대 영상 길이(분) | `3` |
|
|
69
|
+
| `-o`, `--output` | 저장 경로 | `~/Downloads/youtube_collection` |
|
|
70
|
+
| `--fast` | 고속 모드 (병렬 다운로드) | 비활성화 |
|
|
71
|
+
| `-w`, `--workers` | 병렬 다운로드 수 | `3` |
|
|
72
|
+
| `--proxy` | 프록시 주소 | 없음 |
|
|
73
|
+
|
|
74
|
+
### 카테고리 종류
|
|
75
|
+
|
|
76
|
+
| 카테고리 | 설명 | 검색 소스 |
|
|
77
|
+
|----------|------|-----------|
|
|
78
|
+
| `face` | 얼굴/인물 | SBS 인터뷰, 런닝맨, 미운우리새끼 등 |
|
|
79
|
+
| `license_plate` | 자동차 번호판 | 중고차 매물, 세차 영상, 신차 출고 등 |
|
|
80
|
+
| `tattoo` | 타투/문신 | 타투 시술, 타투이스트 작업 영상 |
|
|
81
|
+
| `text` | 텍스트/자막 | SBS 예능 (런닝맨, 골목식당 등) |
|
|
82
|
+
|
|
83
|
+
## 예시
|
|
84
|
+
|
|
85
|
+
### 단일 카테고리
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# 얼굴 영상 10개 수집
|
|
89
|
+
python main.py -c face -n 10
|
|
90
|
+
|
|
91
|
+
# 번호판 영상 수집 (최대 5분)
|
|
92
|
+
python main.py -c license_plate -d 5
|
|
93
|
+
|
|
94
|
+
# 타투 영상 수집
|
|
95
|
+
python main.py -c tattoo -n 5
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 여러 카테고리
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# 얼굴과 텍스트 각 10개씩
|
|
102
|
+
python main.py -c face text -n 10
|
|
103
|
+
|
|
104
|
+
# 모든 카테고리 수집
|
|
105
|
+
python main.py -c face license_plate tattoo text -n 5
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 고속 모드
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# 병렬 다운로드 (기본 3개 동시)
|
|
112
|
+
python main.py -c face -n 10 --fast
|
|
113
|
+
|
|
114
|
+
# 5개 동시 다운로드
|
|
115
|
+
python main.py -c face -n 10 --fast -w 5
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 저장 경로 지정
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
python main.py -c face -o /path/to/save
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 프록시 사용
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
python main.py -c face --proxy http://proxy.server:8080
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## SBS Dataset 구축 (URL 리스트 기반)
|
|
131
|
+
|
|
132
|
+
URL 리스트를 기반으로 영상을 수집하고 특정 시점을 기준으로 자동으로 클리핑(3분 미만)하는 기능입니다.
|
|
133
|
+
|
|
134
|
+
### 실행 방법
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
ytc-dataset youtube_url.txt
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### youtube_url.txt 형식
|
|
141
|
+
|
|
142
|
+
`URL, MM:SS, TaskName` 형식으로 작성합니다.
|
|
143
|
+
```text
|
|
144
|
+
https://www.youtube.com/watch?v=aqz-KE-bpKQ, 00:10, sample_task
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### 상세 옵션
|
|
148
|
+
|
|
149
|
+
| 옵션 | 설명 | 기본값 |
|
|
150
|
+
|------|------|--------|
|
|
151
|
+
| `file` | URL 리스트 파일 경로 | (필수) |
|
|
152
|
+
| `-o`, `--output` | 저장 루트 경로 | `.` |
|
|
153
|
+
|
|
154
|
+
### 특징
|
|
155
|
+
- **자동 트리밍**: 지정된 MM:SS 시점 기준 $\pm$ 1.5분(총 3분)을 자동으로 자릅니다.
|
|
156
|
+
- **중복 방지**: 인덱스 기반으로 이미 다운로드/클리핑된 영상은 건너뜁니다.
|
|
157
|
+
- **저장 구조**: `./video/` (원본), `./video_clips/` (클립) 폴더가 생성됩니다.
|
|
158
|
+
|
|
159
|
+
## 출력 폴더 구조
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
저장경로/
|
|
163
|
+
├── 얼굴/ # 얼굴 감지된 영상
|
|
164
|
+
├── 번호판/ # 번호판 감지된 영상
|
|
165
|
+
├── 번호판_미감지/ # 번호판 미감지 (수동 확인용)
|
|
166
|
+
├── 타투/ # 타투 감지된 영상
|
|
167
|
+
├── 텍스트/ # 텍스트 감지된 영상
|
|
168
|
+
└── .archive.txt # 다운로드 기록 (중복 방지)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## 파일 구조
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
260202_test/
|
|
175
|
+
├── main.py # CLI 진입점
|
|
176
|
+
├── config.py # 설정 (검색어, UA 등)
|
|
177
|
+
├── analyzer.py # 영상 분석 (OpenCV, EasyOCR)
|
|
178
|
+
├── downloader.py # 다운로드 로직
|
|
179
|
+
└── README.md # 사용설명서
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## 분석 기능
|
|
183
|
+
|
|
184
|
+
| 감지 항목 | 사용 기술 | 설명 |
|
|
185
|
+
|-----------|-----------|------|
|
|
186
|
+
| 얼굴 | OpenCV Haar Cascade | 정면 얼굴 감지 |
|
|
187
|
+
| 텍스트 | EasyOCR | 한국어/영어 문자 인식 |
|
|
188
|
+
| 번호판 | EasyOCR + 정규식 | 번호판 패턴 매칭 |
|
|
189
|
+
| 타투 | OpenCV HSV 분석 | 피부 영역 내 잉크 패턴 |
|
|
190
|
+
|
|
191
|
+
## 주의사항
|
|
192
|
+
|
|
193
|
+
- 영상은 다운로드 후 분석하여 해당 카테고리가 감지된 경우에만 저장됩니다
|
|
194
|
+
- 번호판 카테고리는 미감지 영상도 별도 폴더에 보관됩니다 (수동 확인용)
|
|
195
|
+
- 이미 다운로드한 영상은 자동으로 스킵됩니다 (`.archive.txt` 기록)
|
|
196
|
+
- 비공개/삭제/저작권 영상은 자동 스킵됩니다
|
|
197
|
+
|
|
198
|
+
## 고속 모드 vs 일반 모드
|
|
199
|
+
|
|
200
|
+
| 항목 | 일반 모드 | 고속 모드 |
|
|
201
|
+
|------|-----------|-----------|
|
|
202
|
+
| 다운로드 | 순차 | 병렬 |
|
|
203
|
+
| 딜레이 | 0.5~1.5초 | 없음 |
|
|
204
|
+
| 재시도 | 3회 | 1회 |
|
|
205
|
+
| 타임아웃 | 30초 | 10초 |
|
|
206
|
+
|
|
207
|
+
고속 모드는 빠르지만 YouTube 차단 위험이 높아질 수 있습니다.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
ytcollector/__init__.py,sha256=OkibE8GYgt1qwOmkiBNXywkGVdnMj5sVpVzDVPSRXQg,1094
|
|
2
|
+
ytcollector/analyzer.py,sha256=JvppXAcoZ43lXJnGRX-dVGTSZ0QQ-IxBzF6ljT1BjJQ,6388
|
|
3
|
+
ytcollector/cli.py,sha256=zOwnHs7kClOkcWHSUPXrVIPaZYKADMNCBsIosZEzmYc,5629
|
|
4
|
+
ytcollector/config.py,sha256=w5Sx-jKdp4R-rCncDdOXc3WfSuH5OXkVRMIeMXL48VU,2216
|
|
5
|
+
ytcollector/dataset_builder.py,sha256=HGVX_mR1W7_wBl2C5C6Cj43OCVseAGIYmg3-n8WLKuo,4598
|
|
6
|
+
ytcollector/downloader.py,sha256=yQGGTR9ErjHlXHp_RXIDD3Zbl9geTyTHGROPO0nuxV8,12794
|
|
7
|
+
ytcollector-1.0.9.dist-info/METADATA,sha256=bIEbwbhupi-Eo6HQ_4KCPRsM_09d6QK6HAnq2aMiNdM,6212
|
|
8
|
+
ytcollector-1.0.9.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
9
|
+
ytcollector-1.0.9.dist-info/entry_points.txt,sha256=waiVuSJJYt-6_DAal-T4JkHgejo7wKYLdKrEI7tZ-ms,127
|
|
10
|
+
ytcollector-1.0.9.dist-info/top_level.txt,sha256=wozNyCUm0eMOm-9U81yTql6oGaM2O5rWVBXDb93zzyQ,12
|
|
11
|
+
ytcollector-1.0.9.dist-info/RECORD,,
|
config/settings.py
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
SBS Dataset Collection Pipeline - Settings
|
|
3
|
-
"""
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
# Base paths
|
|
7
|
-
BASE_DIR = Path(__file__).parent.parent
|
|
8
|
-
DATA_DIR = BASE_DIR / "data"
|
|
9
|
-
URLS_DIR = DATA_DIR / "urls"
|
|
10
|
-
VIDEOS_DIR = DATA_DIR / "videos"
|
|
11
|
-
CLIPS_DIR = DATA_DIR / "clips"
|
|
12
|
-
OUTPUTS_DIR = BASE_DIR / "outputs"
|
|
13
|
-
REPORTS_DIR = OUTPUTS_DIR / "reports"
|
|
14
|
-
|
|
15
|
-
# Video settings
|
|
16
|
-
CLIP_DURATION_BEFORE = 90 # 1분 30초 (초 단위)
|
|
17
|
-
CLIP_DURATION_AFTER = 90 # 1분 30초 (초 단위)
|
|
18
|
-
MAX_CLIP_DURATION = 180 # 최대 3분
|
|
19
|
-
|
|
20
|
-
# Download settings
|
|
21
|
-
VIDEO_FORMAT = "best[ext=mp4]/best"
|
|
22
|
-
DOWNLOAD_RETRIES = 3
|
|
23
|
-
|
|
24
|
-
# YOLO-World settings
|
|
25
|
-
YOLO_MODEL = "yolov8s-worldv2.pt"
|
|
26
|
-
CONFIDENCE_THRESHOLD = 0.25
|
|
27
|
-
FRAME_SAMPLE_RATE = 30 # 매 30프레임마다 샘플링 (약 1초)
|
|
28
|
-
|
|
29
|
-
# Task-specific class prompts
|
|
30
|
-
TASK_CLASSES = {
|
|
31
|
-
"face": ["human face", "person face", "close-up face"],
|
|
32
|
-
"license_plate": ["car license plate", "vehicle license plate", "korean license plate"],
|
|
33
|
-
"tattoo": ["tattoo", "body tattoo", "skin tattoo"],
|
|
34
|
-
"text": ["text on screen", "subtitle", "korean text", "caption"]
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
# Create directories if not exist
|
|
38
|
-
for dir_path in [URLS_DIR, VIDEOS_DIR, CLIPS_DIR, REPORTS_DIR]:
|
|
39
|
-
dir_path.mkdir(parents=True, exist_ok=True)
|
ytcollector/utils.py
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Utility functions for the SBS Dataset Collection Pipeline
|
|
3
|
-
"""
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from datetime import datetime
|
|
6
|
-
import re
|
|
7
|
-
import json
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def timestamp_to_seconds(minutes: int, seconds: int) -> int:
|
|
11
|
-
"""분:초를 총 초로 변환"""
|
|
12
|
-
return minutes * 60 + seconds
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def seconds_to_timestamp(total_seconds: int) -> str:
|
|
16
|
-
"""초를 MM:SS 형식으로 변환"""
|
|
17
|
-
minutes = total_seconds // 60
|
|
18
|
-
seconds = total_seconds % 60
|
|
19
|
-
return f"{minutes:02d}:{seconds:02d}"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def extract_video_id(url: str) -> str:
|
|
23
|
-
"""YouTube URL에서 video ID 추출"""
|
|
24
|
-
patterns = [
|
|
25
|
-
r'(?:v=|/)([0-9A-Za-z_-]{11}).*',
|
|
26
|
-
r'(?:embed/)([0-9A-Za-z_-]{11})',
|
|
27
|
-
r'(?:youtu\.be/)([0-9A-Za-z_-]{11})',
|
|
28
|
-
]
|
|
29
|
-
|
|
30
|
-
for pattern in patterns:
|
|
31
|
-
match = re.search(pattern, url)
|
|
32
|
-
if match:
|
|
33
|
-
return match.group(1)
|
|
34
|
-
|
|
35
|
-
return url[-11:] if len(url) >= 11 else url
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def ensure_dir(path: Path) -> Path:
|
|
39
|
-
"""디렉토리 생성 (없으면)"""
|
|
40
|
-
try:
|
|
41
|
-
path.mkdir(parents=True, exist_ok=True)
|
|
42
|
-
except PermissionError:
|
|
43
|
-
# 네트워크 드라이브 권한 문제 등
|
|
44
|
-
pass
|
|
45
|
-
return path
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def get_output_dir(base_dir: Path) -> Path:
|
|
49
|
-
"""영상이 저장될 실제 디렉토리 반환"""
|
|
50
|
-
from .config import CUSTOM_OUTPUT_DIR
|
|
51
|
-
|
|
52
|
-
if CUSTOM_OUTPUT_DIR:
|
|
53
|
-
return ensure_dir(Path(CUSTOM_OUTPUT_DIR))
|
|
54
|
-
|
|
55
|
-
# 기본값: 프로젝트 폴더 내 video/ (단일 폴더 모드)
|
|
56
|
-
# 기존에는 video/{task_type}이었으나, 요구사항 변경으로 "한 폴더 안에" 저장
|
|
57
|
-
return ensure_dir(base_dir / "video")
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def get_next_filename(output_dir: Path, task_type: str) -> str:
|
|
61
|
-
"""
|
|
62
|
-
순차적인 파일명 생성 (task_0001.mp4)
|
|
63
|
-
폴더 내의 기존 파일을 스캔하여 가장 큰 번호 + 1 반환
|
|
64
|
-
"""
|
|
65
|
-
# glob은 느릴 수 있으므로 파일이 많아지면 최적화 필요
|
|
66
|
-
# 현재는 100개 제한이므로 괜찮음
|
|
67
|
-
existing_files = list(output_dir.glob(f"{task_type}_*.mp4"))
|
|
68
|
-
max_num = 0
|
|
69
|
-
|
|
70
|
-
pattern = re.compile(rf"{task_type}_(\d{{4}})\.mp4")
|
|
71
|
-
|
|
72
|
-
for file_path in existing_files:
|
|
73
|
-
match = pattern.match(file_path.name)
|
|
74
|
-
if match:
|
|
75
|
-
num = int(match.group(1))
|
|
76
|
-
if num > max_num:
|
|
77
|
-
max_num = num
|
|
78
|
-
|
|
79
|
-
next_num = max_num + 1
|
|
80
|
-
return f"{task_type}_{next_num:04d}"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def get_clip_path(base_dir: Path, task_type: str, filename: str = None) -> Path:
|
|
84
|
-
"""클립 저장 경로 반환"""
|
|
85
|
-
# filename이 None이면 순차적 이름 생성
|
|
86
|
-
output_dir = get_output_dir(base_dir)
|
|
87
|
-
|
|
88
|
-
if filename is None:
|
|
89
|
-
filename_str = get_next_filename(output_dir, task_type)
|
|
90
|
-
return output_dir / f"{filename_str}.mp4"
|
|
91
|
-
|
|
92
|
-
# 확장자 보정
|
|
93
|
-
if not filename.endswith('.mp4'):
|
|
94
|
-
filename += '.mp4'
|
|
95
|
-
|
|
96
|
-
return output_dir / filename
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def get_task_video_count(base_dir: Path, task_type: str) -> int:
|
|
100
|
-
"""해당 태스크의 영상 개수 확인 (파일명 기준)"""
|
|
101
|
-
output_dir = get_output_dir(base_dir)
|
|
102
|
-
return len(list(output_dir.glob(f"{task_type}_*.mp4")))
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def load_history(base_dir: Path) -> dict:
|
|
106
|
-
"""다운로드 히스토리 로드 (URL 중복 방지용)"""
|
|
107
|
-
# 히스토리 파일은 항상 프로젝트 로컬 폴더에 저장 (네트워크 공유 X)
|
|
108
|
-
history_path = base_dir / "download_history.json"
|
|
109
|
-
if history_path.exists():
|
|
110
|
-
try:
|
|
111
|
-
return json.loads(history_path.read_text(encoding='utf-8'))
|
|
112
|
-
except:
|
|
113
|
-
return {}
|
|
114
|
-
return {}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def save_history(base_dir: Path, history: dict):
|
|
118
|
-
"""다운로드 히스토리 저장"""
|
|
119
|
-
history_path = base_dir / "download_history.json"
|
|
120
|
-
history_path.write_text(json.dumps(history, indent=2, ensure_ascii=False), encoding='utf-8')
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def get_url_file_path(base_dir: Path, task_type: str) -> Path:
|
|
124
|
-
"""URL 파일 경로 반환"""
|
|
125
|
-
# URL 파일은 로컬 urls/task_type/youtube_url.txt
|
|
126
|
-
task_dir = ensure_dir(base_dir / "urls" / task_type)
|
|
127
|
-
return task_dir / "youtube_url.txt"
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def get_report_path(base_dir: Path, task_type: str, filename: str) -> Path:
|
|
131
|
-
"""리포트 저장 경로 반환"""
|
|
132
|
-
task_dir = ensure_dir(base_dir / "outputs" / "reports" / task_type)
|
|
133
|
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
134
|
-
return task_dir / f"{filename}_report_{timestamp}.json"
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def validate_url(url: str) -> bool:
|
|
138
|
-
"""YouTube URL 유효성 검사"""
|
|
139
|
-
youtube_patterns = [
|
|
140
|
-
r'(https?://)?(www\.)?youtube\.com/watch\?v=',
|
|
141
|
-
r'(https?://)?(www\.)?youtu\.be/',
|
|
142
|
-
r'(https?://)?(www\.)?youtube\.com/embed/',
|
|
143
|
-
]
|
|
144
|
-
return any(re.match(pattern, url) for pattern in youtube_patterns)
|
ytcollector/verifier.py
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
YOLO-World Verifier Module
|
|
3
|
-
YOLO-World 기반 객체 탐지 및 클래스 검증
|
|
4
|
-
"""
|
|
5
|
-
import json
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import List, Dict, Optional
|
|
8
|
-
import logging
|
|
9
|
-
from datetime import datetime
|
|
10
|
-
|
|
11
|
-
import cv2
|
|
12
|
-
from tqdm import tqdm
|
|
13
|
-
|
|
14
|
-
# Updated imports for new package structure
|
|
15
|
-
from .config import (
|
|
16
|
-
YOLO_MODEL,
|
|
17
|
-
CONFIDENCE_THRESHOLD,
|
|
18
|
-
FRAME_SAMPLE_RATE,
|
|
19
|
-
TASK_CLASSES,
|
|
20
|
-
)
|
|
21
|
-
from .utils import get_report_path
|
|
22
|
-
|
|
23
|
-
logger = logging.getLogger(__name__)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class YOLOWorldVerifier:
|
|
27
|
-
"""YOLO-World 기반 영상 검증 클래스"""
|
|
28
|
-
|
|
29
|
-
def __init__(self, task_type: str, base_dir: Path = None, model_name: str = YOLO_MODEL):
|
|
30
|
-
self.task_type = task_type
|
|
31
|
-
self.base_dir = base_dir or Path.cwd()
|
|
32
|
-
self.model_name = model_name
|
|
33
|
-
self.model = None
|
|
34
|
-
self.classes = TASK_CLASSES.get(task_type, [])
|
|
35
|
-
|
|
36
|
-
if not self.classes:
|
|
37
|
-
raise ValueError(f"Unknown task type: {task_type}")
|
|
38
|
-
|
|
39
|
-
def load_model(self):
|
|
40
|
-
"""YOLO-World 모델 로드"""
|
|
41
|
-
if self.model is None:
|
|
42
|
-
from ultralytics import YOLOWorld
|
|
43
|
-
|
|
44
|
-
logger.info(f"Loading YOLO-World model: {self.model_name}")
|
|
45
|
-
self.model = YOLOWorld(self.model_name)
|
|
46
|
-
|
|
47
|
-
logger.info(f"Setting classes for {self.task_type}: {self.classes}")
|
|
48
|
-
self.model.set_classes(self.classes)
|
|
49
|
-
|
|
50
|
-
return self.model
|
|
51
|
-
|
|
52
|
-
def verify_frame(self, frame) -> List[Dict]:
|
|
53
|
-
"""단일 프레임에서 객체 탐지"""
|
|
54
|
-
model = self.load_model()
|
|
55
|
-
results = model.predict(frame, conf=CONFIDENCE_THRESHOLD, verbose=False)
|
|
56
|
-
|
|
57
|
-
detections = []
|
|
58
|
-
for result in results:
|
|
59
|
-
boxes = result.boxes
|
|
60
|
-
for box in boxes:
|
|
61
|
-
detection = {
|
|
62
|
-
'class_id': int(box.cls[0]),
|
|
63
|
-
'class_name': self.classes[int(box.cls[0])] if int(box.cls[0]) < len(self.classes) else 'unknown',
|
|
64
|
-
'confidence': float(box.conf[0]),
|
|
65
|
-
'bbox': box.xyxy[0].tolist(),
|
|
66
|
-
}
|
|
67
|
-
detections.append(detection)
|
|
68
|
-
|
|
69
|
-
return detections
|
|
70
|
-
|
|
71
|
-
def verify_video(
|
|
72
|
-
self,
|
|
73
|
-
video_path: Path,
|
|
74
|
-
sample_rate: int = FRAME_SAMPLE_RATE
|
|
75
|
-
) -> Dict:
|
|
76
|
-
"""영상 전체 검증"""
|
|
77
|
-
logger.info(f"Verifying video: {video_path}")
|
|
78
|
-
|
|
79
|
-
cap = cv2.VideoCapture(str(video_path))
|
|
80
|
-
|
|
81
|
-
if not cap.isOpened():
|
|
82
|
-
raise ValueError(f"Cannot open video: {video_path}")
|
|
83
|
-
|
|
84
|
-
fps = cap.get(cv2.CAP_PROP_FPS)
|
|
85
|
-
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
86
|
-
duration = total_frames / fps if fps > 0 else 0
|
|
87
|
-
|
|
88
|
-
frame_results = []
|
|
89
|
-
detection_count = 0
|
|
90
|
-
frames_with_detection = 0
|
|
91
|
-
frame_idx = 0
|
|
92
|
-
|
|
93
|
-
pbar = tqdm(total=total_frames // sample_rate, desc="Verifying")
|
|
94
|
-
|
|
95
|
-
while True:
|
|
96
|
-
ret, frame = cap.read()
|
|
97
|
-
if not ret:
|
|
98
|
-
break
|
|
99
|
-
|
|
100
|
-
if frame_idx % sample_rate == 0:
|
|
101
|
-
detections = self.verify_frame(frame)
|
|
102
|
-
|
|
103
|
-
if detections:
|
|
104
|
-
frames_with_detection += 1
|
|
105
|
-
detection_count += len(detections)
|
|
106
|
-
|
|
107
|
-
frame_results.append({
|
|
108
|
-
'frame_idx': frame_idx,
|
|
109
|
-
'timestamp_sec': frame_idx / fps if fps > 0 else 0,
|
|
110
|
-
'detections': detections,
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
pbar.update(1)
|
|
114
|
-
|
|
115
|
-
frame_idx += 1
|
|
116
|
-
|
|
117
|
-
cap.release()
|
|
118
|
-
pbar.close()
|
|
119
|
-
|
|
120
|
-
sampled_frames = max(1, total_frames // sample_rate)
|
|
121
|
-
detection_rate = frames_with_detection / sampled_frames
|
|
122
|
-
|
|
123
|
-
result = {
|
|
124
|
-
'video_path': str(video_path),
|
|
125
|
-
'task_type': self.task_type,
|
|
126
|
-
'classes': self.classes,
|
|
127
|
-
'summary': {
|
|
128
|
-
'total_frames': total_frames,
|
|
129
|
-
'sampled_frames': sampled_frames,
|
|
130
|
-
'fps': fps,
|
|
131
|
-
'duration_sec': duration,
|
|
132
|
-
'frames_with_detection': frames_with_detection,
|
|
133
|
-
'total_detections': detection_count,
|
|
134
|
-
'detection_rate': detection_rate,
|
|
135
|
-
},
|
|
136
|
-
'frame_results': frame_results,
|
|
137
|
-
'verified_at': datetime.now().isoformat(),
|
|
138
|
-
'model': self.model_name,
|
|
139
|
-
'is_valid': detection_rate > 0.01, # 1% 이상 탐지되면 유효한 것으로 간주 (기존 10%에서 하향)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
logger.info(
|
|
143
|
-
f"Verification complete: {frames_with_detection}/{sampled_frames} frames "
|
|
144
|
-
f"({detection_rate:.1%}) with {self.task_type} detected"
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
return result
|
|
148
|
-
|
|
149
|
-
def save_report(self, result: Dict, output_path: Optional[Path] = None) -> Path:
|
|
150
|
-
"""검증 결과 JSON 저장"""
|
|
151
|
-
if output_path is None:
|
|
152
|
-
video_name = Path(result['video_path']).stem
|
|
153
|
-
output_path = get_report_path(self.base_dir, self.task_type, video_name)
|
|
154
|
-
|
|
155
|
-
with open(output_path, 'w', encoding='utf-8') as f:
|
|
156
|
-
json.dump(result, f, ensure_ascii=False, indent=2)
|
|
157
|
-
|
|
158
|
-
logger.info(f"Report saved to: {output_path}")
|
|
159
|
-
return output_path
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def verify_clip(video_path: Path, task_type: str, base_dir: Path = None) -> Dict:
|
|
163
|
-
"""클립 검증 헬퍼 함수"""
|
|
164
|
-
verifier = YOLOWorldVerifier(task_type, base_dir)
|
|
165
|
-
result = verifier.verify_video(video_path)
|
|
166
|
-
verifier.save_report(result)
|
|
167
|
-
return result
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def batch_verify(video_dir: Path, task_type: str, base_dir: Path = None) -> List[Dict]:
|
|
171
|
-
"""디렉토리 내 모든 영상 일괄 검증"""
|
|
172
|
-
verifier = YOLOWorldVerifier(task_type, base_dir)
|
|
173
|
-
results = []
|
|
174
|
-
|
|
175
|
-
video_files = list(video_dir.glob("*.mp4"))
|
|
176
|
-
logger.info(f"Found {len(video_files)} videos to verify")
|
|
177
|
-
|
|
178
|
-
for video_path in video_files:
|
|
179
|
-
try:
|
|
180
|
-
result = verifier.verify_video(video_path)
|
|
181
|
-
verifier.save_report(result)
|
|
182
|
-
results.append(result)
|
|
183
|
-
except Exception as e:
|
|
184
|
-
logger.error(f"Failed to verify {video_path}: {e}")
|
|
185
|
-
results.append({'video_path': str(video_path), 'error': str(e), 'is_valid': False})
|
|
186
|
-
|
|
187
|
-
return results
|