From 29cdeb2adf9ca909e0b835706df4ab622833d394 Mon Sep 17 00:00:00 2001 From: JOUNGWOOK KWON Date: Tue, 7 Apr 2026 21:24:47 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20Android=20=EC=BD=94=EB=8D=B1=20=ED=98=B8?= =?UTF-8?q?=ED=99=98=EC=84=B1=20+=20=EC=8A=A4=ED=86=A1=EC=98=81=EC=83=81?= =?UTF-8?q?=20=EC=8A=A4=ED=81=AC=EB=A6=B0=EB=85=B9=ED=99=94=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=EB=A7=81=20+=20Gitea=20URL=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- CLAUDE.md | 4 +-- bots/shorts/stock_fetcher.py | 48 ++++++++++++++++++++++++++++++++++ bots/shorts/video_assembler.py | 7 +++++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5ff5060..e1caba4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/bots/shorts/stock_fetcher.py b/bots/shorts/stock_fetcher.py index 7886827..758a192 100644 --- a/bots/shorts/stock_fetcher.py +++ b/bots/shorts/stock_fetcher.py @@ -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), ] diff --git a/bots/shorts/video_assembler.py b/bots/shorts/video_assembler.py index 2442c19..a222bb3 100644 --- a/bots/shorts/video_assembler.py +++ b/bots/shorts/video_assembler.py @@ -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: