feat: Reddit 수집, 쇼츠 텔레그램 미리보기, 코너 9개 체계 정비

- Reddit 트렌딩 수집기 추가 (/reddit collect, /pick 명령어)
- 쇼츠 영상 텔레그램 미리보기 후 승인 기반 YouTube 업로드
- 코너 9개로 통합 (앱추천→제품리뷰, 재테크절약→재테크, TV로보는세상/건강정보 추가)
- RSS 피드 73개로 확대 (9개 코너 전체 커버)
- 블로그 중복 검토 알림 수정, 글 잘림 방지 (max_tokens 8192)
- 제품리뷰 다중 이미지 지원, 저품질 이미지 필터링 강화
- HookOptimizer LLM 연동, 인스타/X/틱톡 스케줄러 비활성화

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
JOUNGWOOK KWON
2026-04-07 13:56:20 +09:00
parent 93b2d3a264
commit 726c593e85
15 changed files with 1357 additions and 190 deletions
+2 -2
View File
@@ -53,9 +53,9 @@ CORNER_CAPTION_MAP = {
'스타트업': 'hormozi',
'제품리뷰': 'hormozi',
'생활꿀팁': 'tiktok_viral',
'앱추천': 'brand_4thpath',
'재테크절약': 'hormozi',
'재테크': 'hormozi',
'TV로보는세상': 'tiktok_viral',
'건강정보': 'brand_4thpath',
'팩트체크': 'brand_4thpath',
# 레거시 코너 (하위 호환)
'쉬운세상': 'hormozi',
+12 -6
View File
@@ -171,7 +171,7 @@ def _extract_via_claude_api(post_text: str) -> Optional[dict]:
msg = client.messages.create(
model='claude-haiku-4-5-20251001',
max_tokens=512,
max_tokens=1024,
messages=[{'role': 'user', 'content': prompt}],
)
raw = msg.content[0].text
@@ -198,14 +198,20 @@ def _extract_rule_based(article: dict) -> dict:
if not hook.endswith('?'):
hook = f'{title[:20]}... 알고 계셨나요?'
# body: KEY_POINTS 앞 3개
body = [p.strip('- ').strip() for p in key_points[:3]] if key_points else [title]
# body: KEY_POINTS 앞 7개 (35-45초 분량)
body = [p.strip('- ').strip() for p in key_points[:7]] if key_points else [title]
# closer: 코너별 CTA
cta_map = {
'쉬운세상': '블로그에서 더 자세히 확인해보세요.',
'숨은보물': '이 꿀팁, 주변에 공유해보세요.',
'웹소설': '전편 블로그에서 읽어보세요.',
'AI인사이트': '더 깊은 AI 이야기, 블로그에서 확인하세요.',
'여행맛집': '숨은 맛집 더 보기, 블로그 링크 클릭!',
'스타트업': '스타트업 트렌드, 블로그에서 자세히 보세요.',
'제품리뷰': '실사용 후기 전문은 블로그에서 확인하세요.',
'생활꿀팁': '이 꿀팁, 주변에 공유해보세요.',
'재테크': '재테크 꿀팁 더 보기, 블로그에서 확인!',
'TV로보는세상': '화제의 장면, 블로그에서 더 보세요.',
'건강정보': '건강 정보 더 보기, 블로그에서 확인하세요.',
'팩트체크': '팩트체크 전문은 블로그에서 확인하세요.',
}
closer = cta_map.get(corner, '구독하고 다음 편도 기대해주세요.')
+8 -2
View File
@@ -55,7 +55,10 @@ def _search_pexels(keyword: str, api_key: str, prefer_vertical: bool = True) ->
})
req = urllib.request.Request(
f'{PEXELS_VIDEO_URL}?{params}',
headers={'Authorization': api_key},
headers={
'Authorization': api_key,
'User-Agent': 'Mozilla/5.0 (compatible; BlogWriter/1.0)',
},
)
try:
with urllib.request.urlopen(req, timeout=15) as resp:
@@ -93,7 +96,10 @@ def _search_pixabay(keyword: str, api_key: str, prefer_vertical: bool = True) ->
'video_type': 'film',
'per_page': 10,
})
req = urllib.request.Request(f'{PIXABAY_VIDEO_URL}?{params}')
req = urllib.request.Request(
f'{PIXABAY_VIDEO_URL}?{params}',
headers={'User-Agent': 'Mozilla/5.0 (compatible; BlogWriter/1.0)'},
)
try:
with urllib.request.urlopen(req, timeout=15) as resp:
data = json.loads(resp.read())
+9 -1
View File
@@ -396,7 +396,15 @@ def _tts_edge(text: str, output_path: Path, cfg: dict) -> list[dict]:
communicate = edge_tts.Communicate(text, voice, rate=rate)
await communicate.save(str(mp3_tmp))
asyncio.get_event_loop().run_until_complete(_generate())
try:
loop = asyncio.get_running_loop()
# 이미 루프 안에 있으면 새 스레드에서 실행
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor() as pool:
pool.submit(lambda: asyncio.run(_generate())).result()
except RuntimeError:
# 루프 없음 — 직접 실행
asyncio.run(_generate())
# mp3 → wav
_mp3_to_wav(mp3_tmp, output_path)