Today / app.py
ginipick's picture
Update app.py
3d9dfc6 verified
raw
history blame
27.1 kB
# -*- coding: utf-8 -*-
"""
AI 뉴스 & 허깅페이스 트렌딩 분석 웹 앱 (Flask 버전) - 완전판
파일명: app.py
실행 방법:
1. pip install Flask requests beautifulsoup4 lxml gunicorn
2. python app.py
3. 브라우저에서 http://localhost:8080 접속
프로덕션 실행:
gunicorn -w 4 -b 0.0.0.0:8080 app:app
"""
from flask import Flask, render_template_string, jsonify, request
import requests
from bs4 import BeautifulSoup
import json
from datetime import datetime
from typing import List, Dict, Optional
import os
import sys
# Flask 앱 초기화
app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False # 한글 JSON 지원
# ============================================
# HTML 템플릿 (완전판)
# ============================================
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 뉴스 & 허깅페이스 트렌딩 분석 시스템</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', 'Apple SD Gothic Neo', 'Malgun Gothic', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
color: #333;
min-height: 100vh;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 20px;
padding: 40px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
h1 {
text-align: center;
color: #667eea;
margin-bottom: 10px;
font-size: 2.8em;
font-weight: 800;
}
.subtitle {
text-align: center;
color: #666;
margin-bottom: 40px;
font-size: 1.2em;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 25px;
margin-bottom: 50px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 15px;
text-align: center;
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
transform: translateY(0);
transition: transform 0.3s, box-shadow 0.3s;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 30px rgba(102, 126, 234, 0.6);
}
.stat-number {
font-size: 3.5em;
font-weight: bold;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
.stat-label {
font-size: 1.2em;
opacity: 0.95;
font-weight: 500;
}
.category-section {
margin-bottom: 50px;
}
.category-title {
background: linear-gradient(90deg, #667eea, #764ba2);
color: white;
padding: 18px 25px;
border-radius: 12px;
font-size: 1.6em;
font-weight: 700;
margin-bottom: 25px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
}
.news-item {
background: #f8f9fa;
padding: 25px;
border-radius: 12px;
margin-bottom: 20px;
border-left: 6px solid #667eea;
transition: all 0.3s;
position: relative;
}
.news-item:hover {
transform: translateX(8px);
box-shadow: 0 8px 20px rgba(0,0,0,0.12);
background: #f0f4ff;
}
.news-title {
font-size: 1.3em;
font-weight: 700;
color: #2c3e50;
margin-bottom: 12px;
line-height: 1.5;
}
.news-meta {
color: #7f8c8d;
font-size: 0.95em;
margin-bottom: 15px;
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.news-link {
display: inline-block;
background: #667eea;
color: white;
padding: 10px 20px;
border-radius: 8px;
text-decoration: none;
font-size: 0.95em;
font-weight: 600;
transition: all 0.3s;
}
.news-link:hover {
background: #764ba2;
transform: scale(1.05);
box-shadow: 0 4px 10px rgba(102, 126, 234, 0.4);
}
.hf-section {
background: linear-gradient(135deg, #f0f4ff 0%, #e8f0fe 100%);
padding: 40px;
border-radius: 20px;
margin-top: 50px;
box-shadow: 0 10px 30px rgba(0,0,0,0.08);
}
.hf-title {
font-size: 2.2em;
color: #667eea;
margin-bottom: 30px;
text-align: center;
font-weight: 800;
}
.model-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 25px;
margin-top: 30px;
}
.model-card {
background: white;
padding: 25px;
border-radius: 12px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
transition: all 0.3s;
border-top: 4px solid #667eea;
}
.model-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3);
}
.model-name {
font-weight: 700;
color: #667eea;
margin-bottom: 15px;
font-size: 1.15em;
word-break: break-word;
}
.model-stats {
font-size: 0.95em;
color: #555;
margin-bottom: 15px;
line-height: 1.8;
}
.model-task {
background: #e8f0fe;
color: #667eea;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.85em;
display: inline-block;
margin-bottom: 15px;
font-weight: 600;
}
.button-group {
text-align: center;
margin: 40px 0;
}
.refresh-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 18px 50px;
font-size: 1.2em;
font-weight: 700;
border-radius: 50px;
cursor: pointer;
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
transition: all 0.3s;
margin: 0 10px;
}
.refresh-btn:hover {
transform: scale(1.08);
box-shadow: 0 12px 30px rgba(102, 126, 234, 0.6);
}
.api-btn {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
color: white;
border: none;
padding: 18px 50px;
font-size: 1.2em;
font-weight: 700;
border-radius: 50px;
cursor: pointer;
box-shadow: 0 8px 20px rgba(17, 153, 142, 0.4);
transition: all 0.3s;
margin: 0 10px;
}
.api-btn:hover {
transform: scale(1.08);
box-shadow: 0 12px 30px rgba(17, 153, 142, 0.6);
}
.loading {
text-align: center;
padding: 60px;
font-size: 1.8em;
color: #667eea;
font-weight: 600;
}
.timestamp {
text-align: center;
color: #999;
margin-top: 40px;
font-size: 1em;
padding: 20px;
background: #f8f9fa;
border-radius: 10px;
}
.footer {
text-align: center;
margin-top: 50px;
padding-top: 30px;
border-top: 2px solid #e0e0e0;
color: #666;
}
.badge {
display: inline-block;
background: #ff6b6b;
color: white;
padding: 4px 10px;
border-radius: 12px;
font-size: 0.75em;
font-weight: 600;
margin-left: 8px;
}
@media (max-width: 768px) {
.container {
padding: 20px;
}
h1 {
font-size: 2em;
}
.stats {
grid-template-columns: repeat(2, 1fr);
}
.model-grid {
grid-template-columns: 1fr;
}
.button-group {
display: flex;
flex-direction: column;
gap: 15px;
}
.refresh-btn, .api-btn {
margin: 0;
width: 100%;
}
}
/* 애니메이션 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.news-item {
animation: fadeIn 0.5s ease-out;
}
.model-card {
animation: fadeIn 0.5s ease-out;
}
</style>
</head>
<body>
<div class="container">
<h1>🤖 AI 뉴스 & 허깅페이스 트렌딩</h1>
<p class="subtitle">실시간 AI 산업 동향 분석 시스템 📊</p>
<!-- 통계 카드 -->
<div class="stats">
<div class="stat-card">
<div class="stat-number">{{ stats.total_news }}</div>
<div class="stat-label">📰 총 뉴스</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ stats.categories }}</div>
<div class="stat-label">📁 카테고리</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ stats.hf_models }}</div>
<div class="stat-label">🤗 HF 모델</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ stats.hf_spaces }}</div>
<div class="stat-label">🚀 HF 스페이스</div>
</div>
</div>
<!-- 카테고리별 뉴스 -->
{% for category, articles in news_by_category.items() %}
<div class="category-section">
<div class="category-title">
<span>📌 {{ category }}</span>
<span class="badge">{{ articles|length }}건</span>
</div>
{% for article in articles %}
<div class="news-item">
<div class="news-title">{{ loop.index }}. {{ article.title }}</div>
<div class="news-meta">
<span>📅 {{ article.date }}</span>
<span>📰 {{ article.source }}</span>
</div>
<a href="{{ article.url }}" target="_blank" class="news-link">
🔗 기사 전문 보기
</a>
</div>
{% endfor %}
</div>
{% endfor %}
<!-- 허깅페이스 트렌딩 모델 -->
<div class="hf-section">
<div class="hf-title">🤗 허깅페이스 트렌딩 모델 TOP 10</div>
{% if hf_models|length > 0 %}
<div class="model-grid">
{% for model in hf_models[:10] %}
<div class="model-card">
<div class="model-name">
{{ loop.index }}. {{ model.name }}
</div>
<div class="model-task">
🏷️ {{ model.task }}
</div>
<div class="model-stats">
📊 다운로드: <strong>{{ "{:,}".format(model.downloads) }}</strong><br>
❤️ 좋아요: <strong>{{ "{:,}".format(model.likes) }}</strong>
</div>
<a href="{{ model.url }}" target="_blank" class="news-link">
🔗 모델 페이지 방문
</a>
</div>
{% endfor %}
</div>
{% else %}
<div class="loading">
⚠️ 모델 데이터를 불러오지 못했습니다.
</div>
{% endif %}
</div>
<!-- 버튼 그룹 -->
<div class="button-group">
<button class="refresh-btn" onclick="location.reload()">
🔄 새로고침
</button>
<button class="api-btn" onclick="window.open('/api/data', '_blank')">
📊 JSON API 보기
</button>
</div>
<!-- 타임스탬프 -->
<div class="timestamp">
⏰ 마지막 업데이트: {{ timestamp }}
</div>
<!-- 푸터 -->
<div class="footer">
<p>🤖 AI 뉴스 분석 시스템 v1.0</p>
<p style="margin-top: 10px; font-size: 0.9em;">
데이터 출처: AI Times, Hugging Face
</p>
</div>
</div>
<script>
// 자동 새로고침 (5분마다) - 선택사항
// setTimeout(() => location.reload(), 5 * 60 * 1000);
console.log('✅ AI 뉴스 분석 시스템 로드 완료');
</script>
</body>
</html>
"""
# ============================================
# AINewsAnalyzer 클래스
# ============================================
class AINewsAnalyzer:
"""AI 뉴스 및 허깅페이스 트렌딩 분석기"""
def __init__(self, fireworks_api_key: Optional[str] = None, brave_api_key: Optional[str] = None):
"""
Args:
fireworks_api_key: Fireworks AI API 키 (선택)
brave_api_key: Brave Search API 키 (선택)
"""
self.fireworks_api_key = fireworks_api_key or os.getenv('FIREWORKS_API_KEY')
self.brave_api_key = brave_api_key or os.getenv('BRAVE_API_KEY')
# 뉴스 카테고리 정의
self.categories = {
"산업동향": ["산업", "기업", "투자", "인수", "파트너십", "시장", "MS", "구글", "아마존", "소프트뱅크"],
"기술혁신": ["기술", "모델", "알고리즘", "개발", "연구", "논문", "삼성", "SAIT"],
"제품출시": ["출시", "공개", "발표", "서비스", "제품", "챗GPT", "소라", "팬서"],
"정책규제": ["규제", "정책", "법", "정부", "제재", "EU", "투자"],
"보안이슈": ["보안", "취약점", "해킹", "위험", "프라이버시"],
}
self.huggingface_data = {
"models": [],
"spaces": []
}
self.news_data = []
def fetch_huggingface_trending(self) -> Dict:
"""허깅페이스 트렌딩 모델 수집"""
print("🤗 허깅페이스 트렌딩 정보 수집 중...")
try:
models_url = "https://huggingface.co/api/models"
params = {
'sort': 'trending',
'limit': 30
}
response = requests.get(models_url, params=params, timeout=15)
if response.status_code == 200:
models = response.json()
for model in models[:30]:
self.huggingface_data['models'].append({
'name': model.get('id', 'Unknown'),
'downloads': model.get('downloads', 0),
'likes': model.get('likes', 0),
'task': model.get('pipeline_tag', 'N/A'),
'url': f"https://huggingface.co/{model.get('id', '')}"
})
print(f"✅ {len(self.huggingface_data['models'])}개 트렌딩 모델 수집 완료")
else:
print(f"⚠️ 모델 API 오류: {response.status_code}")
except Exception as e:
print(f"❌ 모델 수집 오류: {e}")
# 샘플 스페이스 데이터
sample_spaces = [
{"name": "Wan2.2-5B", "title": "고품질 비디오 생성", "url": "https://huggingface.co/spaces/"},
{"name": "FLUX-Image", "title": "텍스트→이미지 생성", "url": "https://huggingface.co/spaces/"},
{"name": "DeepSeek-App", "title": "AI 앱 생성기", "url": "https://huggingface.co/spaces/"},
]
self.huggingface_data['spaces'] = sample_spaces
return self.huggingface_data
def create_sample_news(self) -> List[Dict]:
"""오늘의 AI 뉴스 샘플 데이터 (2025-10-10 기준)"""
sample_news = [
{
'title': 'MS "챗GPT 수요 폭증으로 데이터센터 부족...2026년까지 지속"',
'url': 'https://www.aitimes.com/news/articleView.html?idxno=203055',
'date': '10-10 15:10',
'source': 'AI Times',
'category': '산업동향'
},
{
'title': '미국, UAE에 GPU 판매 일부 승인...엔비디아 시총 5조달러 눈앞',
'url': 'https://www.aitimes.com/news/articleView.html?idxno=203053',
'date': '10-10 14:46',
'source': 'AI Times',
'category': '산업동향'
},
{
'title': '오픈AI, 저렴한 챗GPT 고 요금제 아시아 16개국으로 확대',
'url': 'https://www.aitimes.com/news/articleView.html?idxno=203054',
'date': '10-10 14:15',
'source': 'AI Times',
'category': '제품출시'
},
{
'title': '인텔, 18A 공정으로 자체 제작한 노트북용 칩 팬서 레이크 공개',
'url': 'https://www.aitimes.com/news/articleView.html?idxno=203057',
'date': '10-10 14:03',
'source': 'AI Times',
'category': '제품출시'
},
{
'title': '소라, 챗GPT보다 빨리 100만 다운로드 돌파',
'url': 'https://www.aitimes.com/news/articleView.html?idxno=203045',
'date': '10-10 12:55',
'source': 'AI Times',
'category': '제품출시'
},
{
'title': '구글·아마존, 기업용 AI 서비스 나란히 출시',
'url': 'https://www.aitimes.com/news/articleView.html?idxno=203047',
'date': '10-10 12:41',
'source': 'AI Times',
'category': '제품출시'
},
{
'title': '삼성 SAIT, 거대 모델 능가하는 초소형 추론 모델 TRM 공개',
'url': 'https://www.aitimes.com/news/articleView.html?idxno=203035',
'date': '10-09 21:22',
'source': 'AI Times',
'category': '기술혁신'
},
{
'title': '구글, GUI 에이전트 제미나이 2.5 컴퓨터 유즈 공개',
'url': 'https://www.aitimes.com/news/articleView.html?idxno=203039',
'date': '10-09 20:57',
'source': 'AI Times',
'category': '기술혁신'
},
{
'title': 'EU, 핵심 산업 AX 위한 1.6조 규모 투자 계획 발표',
'url': 'https://www.aitimes.com/news/articleView.html?idxno=203041',
'date': '10-09 18:51',
'source': 'AI Times',
'category': '정책규제'
},
{
'title': '소프트뱅크, ABB 로봇 사업부 7.6조원에 인수',
'url': 'https://www.aitimes.com/news/articleView.html?idxno=203034',
'date': '10-09 18:07',
'source': 'AI Times',
'category': '산업동향'
}
]
self.news_data = sample_news
return sample_news
def categorize_news(self, news_list: List[Dict]) -> List[Dict]:
"""뉴스 카테고리 자동 분류"""
for news in news_list:
if 'category' not in news or news['category'] == '기타':
title = news['title'].lower()
news['category'] = "기타"
for category, keywords in self.categories.items():
if any(keyword.lower() in title for keyword in keywords):
news['category'] = category
break
return news_list
def get_data(self) -> Dict:
"""모든 데이터 수집 및 반환"""
# 뉴스 수집
news = self.create_sample_news()
news = self.categorize_news(news)
# 허깅페이스 데이터 수집
hf_data = self.fetch_huggingface_trending()
# 카테고리별로 뉴스 그룹화
news_by_category = {}
for article in news:
category = article['category']
if category not in news_by_category:
news_by_category[category] = []
news_by_category[category].append(article)
# 통계 계산
stats = {
'total_news': len(news),
'categories': len(news_by_category),
'hf_models': len(hf_data['models']),
'hf_spaces': len(hf_data['spaces'])
}
return {
'news_by_category': news_by_category,
'hf_models': hf_data['models'],
'hf_spaces': hf_data['spaces'],
'stats': stats,
'timestamp': datetime.now().strftime('%Y년 %m월 %d일 %H:%M:%S')
}
# ============================================
# Flask 라우트 정의
# ============================================
@app.route('/')
def index():
"""메인 페이지"""
try:
analyzer = AINewsAnalyzer()
data = analyzer.get_data()
return render_template_string(HTML_TEMPLATE, **data)
except Exception as e:
return f"""
<html>
<body style="font-family: Arial; padding: 50px; text-align: center;">
<h1 style="color: #e74c3c;">⚠️ 오류 발생</h1>
<p>데이터를 불러오는 중 오류가 발생했습니다.</p>
<p style="color: #7f8c8d;">{str(e)}</p>
<button onclick="location.reload()" style="padding: 10px 20px; font-size: 16px; margin-top: 20px; cursor: pointer;">
🔄 새로고침
</button>
</body>
</html>
""", 500
@app.route('/api/data')
def api_data():
"""JSON API 엔드포인트"""
try:
analyzer = AINewsAnalyzer()
data = analyzer.get_data()
return jsonify({
'success': True,
'data': data,
'timestamp': datetime.now().isoformat()
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e),
'timestamp': datetime.now().isoformat()
}), 500
@app.route('/health')
def health():
"""헬스 체크 엔드포인트"""
return jsonify({
"status": "healthy",
"service": "AI News Analyzer",
"version": "1.0.0",
"timestamp": datetime.now().isoformat()
})
@app.route('/api/news')
def api_news():
"""뉴스만 반환하는 API"""
try:
analyzer = AINewsAnalyzer()
news = analyzer.create_sample_news()
return jsonify({
'success': True,
'count': len(news),
'news': news
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
@app.route('/api/hf-models')
def api_hf_models():
"""허깅페이스 모델만 반환하는 API"""
try:
analyzer = AINewsAnalyzer()
hf_data = analyzer.fetch_huggingface_trending()
return jsonify({
'success': True,
'count': len(hf_data['models']),
'models': hf_data['models']
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
# ============================================
# 메인 실행
# ============================================
if __name__ == '__main__':
# 환경 변수에서 포트 가져오기 (기본값: 8080)
port = int(os.environ.get('PORT', 8080))
# 환경 변수에서 디버그 모드 설정 (기본값: False)
debug = os.environ.get('DEBUG', 'False').lower() == 'true'
print(f"""
╔════════════════════════════════════════════════════════════╗
║ ║
║ 🤖 AI 뉴스 & 허깅페이스 트렌딩 웹 앱 시작! ║
║ ║
╚════════════════════════════════════════════════════════════╝
🚀 Flask 서버 시작 중...
📍 메인 페이지: http://localhost:{port}
📊 JSON API: http://localhost:{port}/api/data
📰 뉴스 API: http://localhost:{port}/api/news
🤗 모델 API: http://localhost:{port}/api/hf-models
💚 Health Check: http://localhost:{port}/health
{'🐛 디버그 모드: 활성화' if debug else '⚡ 프로덕션 모드: 최적화됨'}
브라우저에서 위 URL을 열어주세요!
종료하려면 Ctrl+C를 누르세요.
""")
try:
app.run(
host='0.0.0.0',
port=port,
debug=debug,
threaded=True
)
except KeyboardInterrupt:
print("\n\n👋 서버를 종료합니다. 안녕히 가세요!")
sys.exit(0)
except Exception as e:
print(f"\n❌ 서버 시작 실패: {e}")
sys.exit(1)