|
|
|
|
|
""" |
|
|
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 |
|
|
|
|
|
|
|
|
app = Flask(__name__) |
|
|
app.config['JSON_AS_ASCII'] = False |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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> |
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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') |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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__': |
|
|
|
|
|
port = int(os.environ.get('PORT', 8080)) |
|
|
|
|
|
|
|
|
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) |