fix: Android 코덱 호환성 + 스톡영상 스크린녹화 필터링 + Gitea URL 업데이트
- video_assembler: yuv420p, profile high, level 4.0, movflags faststart 추가 - stock_fetcher: AI/UI 키워드 실사영상으로 변환, 스크린녹화 태그 차단 - CLAUDE.md: Gitea URL https://gitea.gru.farm/ 으로 변경 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,10 +10,10 @@
|
||||
|
||||
## 저장소
|
||||
- Git 서버: Gitea (자체 NAS 운영)
|
||||
- Gitea URL: http://nas.gru.farm:3001
|
||||
- Gitea URL: https://gitea.gru.farm/
|
||||
- 계정: airkjw
|
||||
- 저장소: blog-writer
|
||||
- Remote: http://nas.gru.farm:3001/airkjw/blog-writer
|
||||
- Remote: https://gitea.gru.farm/airkjw/blog-writer
|
||||
- 토큰: 8a8842a56866feab3a44b9f044491bf0dfc44963
|
||||
|
||||
## NAS
|
||||
|
||||
@@ -25,6 +25,45 @@ BASE_DIR = Path(__file__).parent.parent.parent
|
||||
PEXELS_VIDEO_URL = 'https://api.pexels.com/videos/search'
|
||||
PIXABAY_VIDEO_URL = 'https://pixabay.com/api/videos/'
|
||||
|
||||
# 스크린 녹화/UI/텍스트 영상 제외 키워드
|
||||
_SCREEN_BLOCK_TAGS = {
|
||||
'screen recording', 'screenshot', 'tutorial', 'demo', 'interface',
|
||||
'typing', 'chatgpt', 'chatbot', 'website', 'browser', 'desktop',
|
||||
'laptop screen', 'phone screen', 'app', 'software', 'code', 'coding',
|
||||
'monitor', 'computer screen', 'ui', 'ux', 'dashboard',
|
||||
}
|
||||
|
||||
# 검색어에서 제외할 키워드 (스크린 녹화 유발)
|
||||
_SEARCH_EXCLUDE = {
|
||||
'chatgpt', 'ai chat', 'gpt', 'openai', 'claude', 'gemini',
|
||||
'software', 'app', 'website', 'browser', 'code',
|
||||
}
|
||||
|
||||
|
||||
def _sanitize_keyword(keyword: str) -> str:
|
||||
"""스크린 녹화 유발 키워드를 자연 영상 키워드로 변환."""
|
||||
kw_lower = keyword.lower()
|
||||
for excl in _SEARCH_EXCLUDE:
|
||||
if excl in kw_lower:
|
||||
# AI/기술 키워드 → 실사 대체 키워드
|
||||
replacements = {
|
||||
'chatgpt': 'futuristic technology',
|
||||
'ai chat': 'artificial intelligence robot',
|
||||
'gpt': 'digital innovation',
|
||||
'openai': 'technology innovation',
|
||||
'claude': 'digital brain',
|
||||
'gemini': 'space stars',
|
||||
'software': 'digital technology',
|
||||
'app': 'smartphone lifestyle',
|
||||
'website': 'modern office',
|
||||
'browser': 'modern workspace',
|
||||
'code': 'digital network',
|
||||
}
|
||||
for k, v in replacements.items():
|
||||
if k in kw_lower:
|
||||
return v
|
||||
return keyword
|
||||
|
||||
|
||||
def _load_config() -> dict:
|
||||
cfg_path = BASE_DIR / 'config' / 'shorts_config.json'
|
||||
@@ -47,6 +86,8 @@ def _search_pexels(keyword: str, api_key: str, prefer_vertical: bool = True) ->
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
keyword = _sanitize_keyword(keyword)
|
||||
|
||||
params = urllib.parse.urlencode({
|
||||
'query': keyword,
|
||||
'orientation': 'portrait' if prefer_vertical else 'landscape',
|
||||
@@ -90,6 +131,8 @@ def _search_pixabay(keyword: str, api_key: str, prefer_vertical: bool = True) ->
|
||||
"""Pixabay Video API 검색 → [{url, width, height, duration}, ...] 반환."""
|
||||
import urllib.parse
|
||||
|
||||
keyword = _sanitize_keyword(keyword)
|
||||
|
||||
params = urllib.parse.urlencode({
|
||||
'key': api_key,
|
||||
'q': keyword,
|
||||
@@ -105,6 +148,10 @@ def _search_pixabay(keyword: str, api_key: str, prefer_vertical: bool = True) ->
|
||||
data = json.loads(resp.read())
|
||||
results = []
|
||||
for hit in data.get('hits', []):
|
||||
# 태그 기반 스크린녹화/UI 영상 필터링
|
||||
tags = hit.get('tags', '').lower()
|
||||
if any(block in tags for block in _SCREEN_BLOCK_TAGS):
|
||||
continue
|
||||
videos = hit.get('videos', {})
|
||||
# medium 우선
|
||||
for quality in ('medium', 'large', 'small', 'tiny'):
|
||||
@@ -157,6 +204,7 @@ def _prepare_clip(input_path: Path, output_path: Path, duration: float = 6.0) ->
|
||||
),
|
||||
'-r', '30',
|
||||
'-c:v', 'libx264', '-crf', '23', '-preset', 'fast',
|
||||
'-pix_fmt', 'yuv420p',
|
||||
'-an', # 스톡 클립 오디오 제거
|
||||
str(output_path),
|
||||
]
|
||||
|
||||
@@ -128,6 +128,7 @@ def _concat_with_xfade(clips: list[Path], output: Path, crossfade: float, ffmpeg
|
||||
'-filter_complex', filter_complex,
|
||||
'-map', prev_label,
|
||||
'-c:v', 'libx264', '-crf', '23', '-preset', 'fast',
|
||||
'-pix_fmt', 'yuv420p',
|
||||
'-an', '-r', '30',
|
||||
str(output),
|
||||
]
|
||||
@@ -151,6 +152,7 @@ def _concat_simple(clips: list[Path], output: Path, ffmpeg: str) -> bool:
|
||||
'-f', 'concat', '-safe', '0',
|
||||
'-i', str(list_file),
|
||||
'-c:v', 'libx264', '-crf', '23', '-preset', 'fast',
|
||||
'-pix_fmt', 'yuv420p',
|
||||
'-an', '-r', '30',
|
||||
str(output),
|
||||
]
|
||||
@@ -240,8 +242,11 @@ def _assemble_final(
|
||||
f'apad=pad_dur=0.2' # 루프 최적화: 0.2s 무음
|
||||
),
|
||||
'-c:v', codec, '-crf', str(crf), '-preset', 'medium',
|
||||
'-pix_fmt', 'yuv420p',
|
||||
'-profile:v', 'high', '-level', '4.0',
|
||||
'-c:a', audio_codec, '-b:a', audio_bitrate,
|
||||
'-r', str(vid_cfg.get('fps', 30)),
|
||||
'-movflags', '+faststart',
|
||||
'-shortest',
|
||||
str(output),
|
||||
]
|
||||
@@ -266,7 +271,9 @@ def _rerender_smaller(src: Path, dst: Path, ffmpeg: str) -> bool:
|
||||
cmd = [
|
||||
ffmpeg, '-y', '-i', str(src),
|
||||
'-c:v', 'libx264', '-crf', '23', '-preset', 'medium',
|
||||
'-pix_fmt', 'yuv420p', '-profile:v', 'high', '-level', '4.0',
|
||||
'-c:a', 'aac', '-b:a', '128k',
|
||||
'-movflags', '+faststart',
|
||||
str(dst),
|
||||
]
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user