Today / app.py
ginipick's picture
Update app.py
48b33a3 verified
raw
history blame
64 kB
# -*- coding: utf-8 -*-
"""
AI λ‰΄μŠ€ & ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© LLM 뢄석 μ›Ήμ•± (μ™„μ „νŒ v3.2)
파일λͺ…: app_advanced.py
μ£Όμš” κΈ°λŠ₯:
1. SQLite DB 영ꡬ μŠ€ν† λ¦¬μ§€
2. AI Times μ‹€μ‹œκ°„ λ‰΄μŠ€ 크둀링 (2개 μ„Ήμ…˜)
3. μ‹€μ œ Hugging Face Trending API 연동 (λͺ¨λΈ/슀페이슀 30μœ„)
4. Fireworks AI (Qwen3-235B) μ‹€μ‹œκ°„ LLM 뢄석
- λ‰΄μŠ€ μ΄ˆλ“±ν•™μƒ μˆ˜μ€€ 뢄석
- λͺ¨λΈ μΉ΄λ“œ μžλ™ 뢄석 (README.md)
- 슀페이슀 μ½”λ“œ μžλ™ 뢄석 (app.py)
5. νƒ­ UI (λ‰΄μŠ€/λͺ¨λΈ/슀페이슀)
μ‹€ν–‰ 방법:
1. pip install Flask requests beautifulsoup4 huggingface_hub
2. export FIREWORKS_API_KEY="your-api-key-here" # 선택사항 (μ—†μœΌλ©΄ ν…œν”Œλ¦Ώ λͺ¨λ“œ)
3. python app_advanced.py
4. λΈŒλΌμš°μ €μ—μ„œ http://localhost:7860 접속
ν™˜κ²½λ³€μˆ˜:
- FIREWORKS_API_KEY: Fireworks AI API ν‚€ (선택, 더 λ‚˜μ€ 뢄석)
- PORT: μ„œλ²„ 포트 (κΈ°λ³Έκ°’: 7860)
"""
from flask import Flask, render_template_string, jsonify, request
import requests
import json
from datetime import datetime
from typing import List, Dict, Optional
import os
import sys
import sqlite3
import time
from huggingface_hub import HfApi
from bs4 import BeautifulSoup
import re
# Flask μ•± μ΄ˆκΈ°ν™”
app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False
# λ°μ΄ν„°λ² μ΄μŠ€ 파일 경둜
DB_PATH = 'ai_news_analysis.db'
# ============================================
# HTML ν…œν”Œλ¦Ώ (νƒ­ UI 포함)
# ============================================
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 λ‰΄μŠ€ & ν—ˆκΉ…νŽ˜μ΄μŠ€ LLM 뢄석 μ‹œμŠ€ν…œ</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;
}
/* νƒ­ μŠ€νƒ€μΌ */
.tabs {
display: flex;
gap: 15px;
margin-bottom: 30px;
border-bottom: 3px solid #e0e0e0;
padding-bottom: 0;
}
.tab {
padding: 15px 30px;
background: #f5f5f5;
border: none;
border-radius: 10px 10px 0 0;
cursor: pointer;
font-size: 1.1em;
font-weight: 600;
color: #666;
transition: all 0.3s;
}
.tab.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.tab:hover {
background: #e0e0e0;
}
.tab.active:hover {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
animation: fadeIn 0.5s ease-out;
}
/* 톡계 μΉ΄λ“œ */
.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;
}
/* λ‰΄μŠ€ μΉ΄λ“œ (LLM 뢄석 버전) */
.news-card {
background: white;
border-radius: 15px;
padding: 30px;
margin-bottom: 25px;
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
border-left: 6px solid #667eea;
transition: all 0.3s;
}
.news-card:hover {
transform: translateX(10px);
box-shadow: 0 10px 30px rgba(0,0,0,0.15);
}
.news-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 15px;
}
.news-title {
font-size: 1.4em;
font-weight: 700;
color: #2c3e50;
flex: 1;
min-width: 300px;
}
.news-meta {
display: flex;
gap: 15px;
color: #7f8c8d;
font-size: 0.9em;
}
.analysis-section {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
margin-top: 15px;
}
.analysis-item {
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #e0e0e0;
}
.analysis-item:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.analysis-label {
display: inline-block;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 8px 15px;
border-radius: 20px;
font-size: 0.9em;
font-weight: 600;
margin-bottom: 10px;
}
.analysis-content {
color: #34495e;
line-height: 1.8;
font-size: 1.05em;
}
.impact-level {
display: inline-block;
padding: 5px 12px;
border-radius: 15px;
font-size: 0.85em;
font-weight: 600;
margin-left: 10px;
}
.impact-high {
background: #ff6b6b;
color: white;
}
.impact-medium {
background: #ffa502;
color: white;
}
.impact-low {
background: #26de81;
color: white;
}
/* λͺ¨λΈ μΉ΄λ“œ */
.model-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 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;
position: relative;
}
.model-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3);
}
.model-rank {
position: absolute;
top: -15px;
right: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 1.2em;
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.model-name {
font-weight: 700;
color: #667eea;
margin-bottom: 15px;
font-size: 1.15em;
word-break: break-word;
padding-right: 60px;
}
.model-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin: 15px 0;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.model-stat-item {
font-size: 0.9em;
}
.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;
}
.model-analysis {
background: #f0f4ff;
padding: 15px;
border-radius: 8px;
margin-top: 15px;
color: #34495e;
line-height: 1.7;
font-size: 0.95em;
}
/* 슀페이슀 μΉ΄λ“œ */
.space-card {
background: white;
padding: 25px;
border-radius: 12px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
margin-bottom: 20px;
border-left: 5px solid #ff6b6b;
transition: all 0.3s;
}
.space-card:hover {
transform: translateX(10px);
box-shadow: 0 10px 25px rgba(255, 107, 107, 0.3);
}
.space-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 15px;
}
.space-name {
font-weight: 700;
color: #ff6b6b;
font-size: 1.3em;
}
.space-badge {
background: #ff6b6b;
color: white;
padding: 5px 12px;
border-radius: 15px;
font-size: 0.8em;
font-weight: 600;
}
.space-description {
color: #555;
margin-bottom: 15px;
line-height: 1.6;
}
.space-analysis {
background: #fff5f5;
padding: 15px;
border-radius: 8px;
margin-top: 15px;
}
.space-tech {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 15px;
}
.tech-tag {
background: #ffe5e5;
color: #ff6b6b;
padding: 5px 10px;
border-radius: 12px;
font-size: 0.8em;
font-weight: 600;
}
/* λ²„νŠΌ */
.button-group {
text-align: center;
margin: 40px 0;
display: flex;
justify-content: center;
gap: 15px;
flex-wrap: wrap;
}
.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;
}
.refresh-btn:hover {
transform: scale(1.08);
box-shadow: 0 12px 30px rgba(102, 126, 234, 0.6);
}
.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;
margin-top: 15px;
}
.news-link:hover {
background: #764ba2;
transform: scale(1.05);
}
.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;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 768px) {
.container {
padding: 20px;
}
h1 {
font-size: 2em;
}
.tabs {
flex-direction: column;
}
.tab {
width: 100%;
}
.model-grid {
grid-template-columns: 1fr;
}
.button-group {
flex-direction: column;
}
.refresh-btn {
width: 100%;
}
}
</style>
</head>
<body>
<div class="container">
<h1>πŸ€– AI λ‰΄μŠ€ & ν—ˆκΉ…νŽ˜μ΄μŠ€ LLM 뢄석</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.hf_models }}</div>
<div class="stat-label">πŸ€— νŠΈλ Œλ”© λͺ¨λΈ</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ stats.hf_spaces }}</div>
<div class="stat-label">πŸš€ 인기 슀페이슀</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ stats.llm_analyses }}</div>
<div class="stat-label">🧠 LLM 뢄석</div>
</div>
</div>
<!-- νƒ­ 메뉴 -->
<div class="tabs">
<button class="tab active" onclick="switchTab('news')">πŸ“° AI λ‰΄μŠ€ 뢄석</button>
<button class="tab" onclick="switchTab('models')">πŸ€— νŠΈλ Œλ”© λͺ¨λΈ</button>
<button class="tab" onclick="switchTab('spaces')">πŸš€ 인기 슀페이슀</button>
</div>
<!-- λ‰΄μŠ€ νƒ­ -->
<div id="news-content" class="tab-content active">
{% for article in analyzed_news %}
<div class="news-card">
<div class="news-header">
<div class="news-title">{{ loop.index }}. {{ article.title }}</div>
<div class="news-meta">
<span>πŸ“… {{ article.date }}</span>
<span>πŸ“° {{ article.source }}</span>
</div>
</div>
<div class="analysis-section">
<div class="analysis-item">
<span class="analysis-label">🎯 μ‰¬μš΄ μš”μ•½</span>
<div class="analysis-content">{{ article.analysis.summary }}</div>
</div>
<div class="analysis-item">
<span class="analysis-label">πŸ’‘ μ™œ μ€‘μš”ν• κΉŒ?</span>
<div class="analysis-content">{{ article.analysis.significance }}</div>
</div>
<div class="analysis-item">
<span class="analysis-label">πŸ“Š 영ν–₯도</span>
<span class="impact-level impact-{{ article.analysis.impact_level }}">
{{ article.analysis.impact_text }}
</span>
<div class="analysis-content" style="margin-top: 10px;">
{{ article.analysis.impact_description }}
</div>
</div>
<div class="analysis-item">
<span class="analysis-label">βœ… μš°λ¦¬κ°€ ν•  수 μžˆλŠ” 것</span>
<div class="analysis-content">{{ article.analysis.action }}</div>
</div>
</div>
<a href="{{ article.url }}" target="_blank" class="news-link">
πŸ”— 전체 기사 읽어보기
</a>
</div>
{% endfor %}
</div>
<!-- λͺ¨λΈ νƒ­ -->
<div id="models-content" class="tab-content">
<div class="model-grid">
{% for model in analyzed_models %}
<div class="model-card">
<div class="model-rank">{{ model.rank }}</div>
<div class="model-name">{{ model.name }}</div>
<div class="model-task">🏷️ {{ model.task }}</div>
<div class="model-stats">
<div class="model-stat-item">
<strong>πŸ“₯ λ‹€μš΄λ‘œλ“œ</strong><br>
{{ "{:,}".format(model.downloads) }}
</div>
<div class="model-stat-item">
<strong>❀️ μ’‹μ•„μš”</strong><br>
{{ "{:,}".format(model.likes) }}
</div>
</div>
<div class="model-analysis">
<strong>🧠 AI 뢄석:</strong><br>
{{ model.analysis }}
</div>
<a href="{{ model.url }}" target="_blank" class="news-link">
πŸ”— λͺ¨λΈ νŽ˜μ΄μ§€ λ°©λ¬Έ
</a>
</div>
{% endfor %}
</div>
{% if analyzed_models|length == 0 %}
<div class="loading">
⚠️ λͺ¨λΈ 데이터λ₯Ό λΆˆλŸ¬μ˜€λŠ” 쀑...<br>
<button onclick="location.href='/?refresh=true'" style="margin-top: 20px; padding: 15px 30px; font-size: 1.1em; cursor: pointer; background: #667eea; color: white; border: none; border-radius: 25px;">
πŸ”₯ 데이터 μˆ˜μ§‘ν•˜κΈ°
</button>
</div>
{% endif %}
</div>
<!-- 슀페이슀 νƒ­ -->
<div id="spaces-content" class="tab-content">
{% for space in analyzed_spaces %}
<div class="space-card">
<div class="space-header">
<div class="space-name">{{ space.rank }}. {{ space.name }}</div>
<span class="space-badge">νŠΈλ Œλ”© {{ space.rank }}μœ„</span>
</div>
<div class="space-description">
<strong>πŸ“ μ„€λͺ…:</strong> {{ space.description }}
</div>
<div class="space-analysis">
<strong>πŸŽ“ μ΄ˆλ“±ν•™μƒ μ„€λͺ…:</strong><br>
{{ space.simple_explanation }}
</div>
{% if space.tech_stack %}
<div class="space-tech">
<strong style="width: 100%; margin-bottom: 5px;">πŸ› οΈ μ‚¬μš© 기술:</strong>
{% for tech in space.tech_stack %}
<span class="tech-tag">{{ tech }}</span>
{% endfor %}
</div>
{% endif %}
<a href="{{ space.url }}" target="_blank" class="news-link">
πŸ”— 슀페이슀 μ²΄ν—˜ν•˜κΈ°
</a>
</div>
{% endfor %}
{% if analyzed_spaces|length == 0 %}
<div class="loading">
⚠️ 슀페이슀 데이터λ₯Ό λΆˆλŸ¬μ˜€λŠ” 쀑...<br>
<button onclick="location.href='/?refresh=true'" style="margin-top: 20px; padding: 15px 30px; font-size: 1.1em; cursor: pointer; background: #ff6b6b; color: white; border: none; border-radius: 25px;">
πŸ”₯ 데이터 μˆ˜μ§‘ν•˜κΈ°
</button>
</div>
{% endif %}
</div>
<!-- λ²„νŠΌ κ·Έλ£Ή -->
<div class="button-group">
<button class="refresh-btn" onclick="location.reload()">
πŸ”„ νŽ˜μ΄μ§€ μƒˆλ‘œκ³ μΉ¨
</button>
<button class="refresh-btn" onclick="location.href='/?refresh=true'" style="background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);">
πŸ”₯ 데이터 κ°•μ œ κ°±μ‹ 
</button>
</div>
<!-- νƒ€μž„μŠ€νƒ¬ν”„ -->
<div class="timestamp">
⏰ λ§ˆμ§€λ§‰ μ—…λ°μ΄νŠΈ: {{ timestamp }}
</div>
<!-- ν‘Έν„° -->
<div class="footer">
<p>πŸ€– AI λ‰΄μŠ€ LLM 뢄석 μ‹œμŠ€ν…œ v3.2</p>
<p style="margin-top: 10px; font-size: 0.9em;">
πŸ’Ύ SQLite DB 영ꡬ μ €μž₯ | 🌐 AI Times μ‹€μ‹œκ°„ 크둀링 | πŸ€— Hugging Face Trending API | 🧠 Powered by Fireworks AI (Qwen3-235B)
</p>
<p style="margin-top: 10px; font-size: 0.85em; color: #999;">
데이터 좜처: AI Times (μ‹€μ‹œκ°„ 크둀링), Hugging Face | μ‹€μ‹œκ°„ 뢄석: Fireworks AI
</p>
</div>
</div>
<script>
function switchTab(tabName) {
// λͺ¨λ“  νƒ­ λΉ„ν™œμ„±ν™”
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
});
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
// μ„ νƒλœ νƒ­ ν™œμ„±ν™”
event.target.classList.add('active');
document.getElementById(tabName + '-content').classList.add('active');
}
console.log('βœ… AI λ‰΄μŠ€ LLM 뢄석 μ‹œμŠ€ν…œ λ‘œλ“œ μ™„λ£Œ');
</script>
</body>
</html>
"""
# ============================================
# λ°μ΄ν„°λ² μ΄μŠ€ μ΄ˆκΈ°ν™”
# ============================================
def init_database():
"""SQLite λ°μ΄ν„°λ² μ΄μŠ€ μ΄ˆκΈ°ν™”"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# λ‰΄μŠ€ ν…Œμ΄λΈ”
cursor.execute('''
CREATE TABLE IF NOT EXISTS news (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
url TEXT NOT NULL UNIQUE,
date TEXT,
source TEXT,
category TEXT,
summary TEXT,
significance TEXT,
impact_level TEXT,
impact_text TEXT,
impact_description TEXT,
action TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# λͺ¨λΈ ν…Œμ΄λΈ”
cursor.execute('''
CREATE TABLE IF NOT EXISTS models (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
downloads INTEGER,
likes INTEGER,
task TEXT,
url TEXT,
analysis TEXT,
rank INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 슀페이슀 ν…Œμ΄λΈ”
cursor.execute('''
CREATE TABLE IF NOT EXISTS spaces (
id INTEGER PRIMARY KEY AUTOINCREMENT,
space_id TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
author TEXT,
title TEXT,
likes INTEGER,
url TEXT,
sdk TEXT,
simple_explanation TEXT,
tech_stack TEXT,
rank INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
conn.close()
print("βœ… λ°μ΄ν„°λ² μ΄μŠ€ μ΄ˆκΈ°ν™” μ™„λ£Œ")
def save_news_to_db(news_list: List[Dict]):
"""λ‰΄μŠ€ 데이터λ₯Ό DB에 μ €μž₯"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
saved_count = 0
for news in news_list:
try:
cursor.execute('''
INSERT OR REPLACE INTO news
(title, url, date, source, category, summary, significance,
impact_level, impact_text, impact_description, action)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
news['title'],
news['url'],
news.get('date', ''),
news.get('source', ''),
news.get('category', ''),
news['analysis']['summary'],
news['analysis']['significance'],
news['analysis']['impact_level'],
news['analysis']['impact_text'],
news['analysis']['impact_description'],
news['analysis']['action']
))
saved_count += 1
except sqlite3.IntegrityError:
pass # 이미 μ‘΄μž¬ν•˜λŠ” λ‰΄μŠ€
conn.commit()
conn.close()
print(f"βœ… {saved_count}개 λ‰΄μŠ€ DB μ €μž₯ μ™„λ£Œ")
def save_models_to_db(models_list: List[Dict]):
"""λͺ¨λΈ 데이터λ₯Ό DB에 μ €μž₯"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
saved_count = 0
for model in models_list:
try:
cursor.execute('''
INSERT OR REPLACE INTO models
(name, downloads, likes, task, url, analysis, rank, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
''', (
model['name'],
model['downloads'],
model['likes'],
model['task'],
model['url'],
model['analysis'],
model['rank']
))
saved_count += 1
except Exception as e:
print(f"⚠️ λͺ¨λΈ μ €μž₯ 였λ₯˜: {e}")
conn.commit()
conn.close()
print(f"βœ… {saved_count}개 λͺ¨λΈ DB μ €μž₯ μ™„λ£Œ")
def save_spaces_to_db(spaces_list: List[Dict]):
"""슀페이슀 데이터λ₯Ό DB에 μ €μž₯"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
saved_count = 0
for space in spaces_list:
try:
cursor.execute('''
INSERT OR REPLACE INTO spaces
(space_id, name, author, title, likes, url, sdk,
simple_explanation, tech_stack, rank, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
''', (
space['space_id'],
space['name'],
space.get('author', ''),
space.get('title', ''),
space.get('likes', 0),
space['url'],
space.get('sdk', ''),
space['simple_explanation'],
json.dumps(space.get('tech_stack', [])),
space['rank']
))
saved_count += 1
except Exception as e:
print(f"⚠️ 슀페이슀 μ €μž₯ 였λ₯˜: {e}")
conn.commit()
conn.close()
print(f"βœ… {saved_count}개 슀페이슀 DB μ €μž₯ μ™„λ£Œ")
def load_news_from_db() -> List[Dict]:
"""DBμ—μ„œ λ‰΄μŠ€ λ‘œλ“œ"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute('''
SELECT title, url, date, source, category, summary, significance,
impact_level, impact_text, impact_description, action
FROM news ORDER BY created_at DESC LIMIT 50
''')
news_list = []
for row in cursor.fetchall():
news_list.append({
'title': row[0],
'url': row[1],
'date': row[2],
'source': row[3],
'category': row[4],
'analysis': {
'summary': row[5],
'significance': row[6],
'impact_level': row[7],
'impact_text': row[8],
'impact_description': row[9],
'action': row[10]
}
})
conn.close()
return news_list
def load_models_from_db() -> List[Dict]:
"""DBμ—μ„œ λͺ¨λΈ λ‘œλ“œ"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute('''
SELECT name, downloads, likes, task, url, analysis, rank
FROM models ORDER BY rank ASC LIMIT 30
''')
models_list = []
for row in cursor.fetchall():
models_list.append({
'name': row[0],
'downloads': row[1],
'likes': row[2],
'task': row[3],
'url': row[4],
'analysis': row[5],
'rank': row[6]
})
conn.close()
return models_list
def load_spaces_from_db() -> List[Dict]:
"""DBμ—μ„œ 슀페이슀 λ‘œλ“œ"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute('''
SELECT space_id, name, author, title, likes, url, sdk,
simple_explanation, tech_stack, rank
FROM spaces ORDER BY rank ASC LIMIT 30
''')
spaces_list = []
for row in cursor.fetchall():
spaces_list.append({
'space_id': row[0],
'name': row[1],
'author': row[2],
'title': row[3],
'likes': row[4],
'url': row[5],
'sdk': row[6],
'simple_explanation': row[7],
'tech_stack': json.loads(row[8]) if row[8] else [],
'rank': row[9],
'description': row[3] # title을 description으둜 μ‚¬μš©
})
conn.close()
return spaces_list
# ============================================
# LLM 뢄석기 클래슀
# ============================================
class LLMAnalyzer:
"""Fireworks AI (Qwen3) 기반 LLM 뢄석기"""
def __init__(self):
self.api_key = os.environ.get('FIREWORKS_API_KEY', '')
self.api_url = "https://api.fireworks.ai/inference/v1/chat/completions"
self.api_available = bool(self.api_key)
if not self.api_available:
print("⚠️ FIREWORKS_API_KEY ν™˜κ²½λ³€μˆ˜κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. ν…œν”Œλ¦Ώ λͺ¨λ“œλ‘œ λ™μž‘ν•©λ‹ˆλ‹€.")
def call_llm(self, messages: List[Dict], max_tokens: int = 2000) -> str:
"""Fireworks AI API 호좜"""
if not self.api_available:
return None
try:
payload = {
"model": "accounts/fireworks/models/qwen3-235b-a22b-instruct-2507",
"max_tokens": max_tokens,
"top_p": 1,
"top_k": 40,
"presence_penalty": 0,
"frequency_penalty": 0,
"temperature": 0.6,
"messages": messages
}
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}"
}
response = requests.post(self.api_url, headers=headers, json=payload, timeout=30)
response.raise_for_status()
result = response.json()
return result['choices'][0]['message']['content']
except Exception as e:
print(f" ⚠️ LLM API 호좜 였λ₯˜: {e}")
return None
def fetch_model_card(self, model_id: str) -> str:
"""ν—ˆκΉ…νŽ˜μ΄μŠ€ λͺ¨λΈ μΉ΄λ“œ(README.md) κ°€μ Έμ˜€κΈ°"""
try:
url = f"https://huggingface.co/{model_id}/raw/main/README.md"
response = requests.get(url, timeout=10)
if response.status_code == 200:
content = response.text
# λ„ˆλ¬΄ κΈ΄ 경우 μ•žλΆ€λΆ„λ§Œ (μ•½ 3000자)
if len(content) > 3000:
content = content[:3000] + "\n...(ν›„λž΅)"
return content
else:
return None
except Exception as e:
print(f" ⚠️ λͺ¨λΈ μΉ΄λ“œ κ°€μ Έμ˜€κΈ° 였λ₯˜: {e}")
return None
def fetch_space_code(self, space_id: str) -> str:
"""ν—ˆκΉ…νŽ˜μ΄μŠ€ 슀페이슀 app.py κ°€μ Έμ˜€κΈ°"""
try:
url = f"https://huggingface.co/spaces/{space_id}/raw/main/app.py"
response = requests.get(url, timeout=10)
if response.status_code == 200:
content = response.text
# λ„ˆλ¬΄ κΈ΄ 경우 μ•žλΆ€λΆ„λ§Œ (μ•½ 2000자)
if len(content) > 2000:
content = content[:2000] + "\n...(ν›„λž΅)"
return content
else:
return None
except Exception as e:
print(f" ⚠️ 슀페이슀 μ½”λ“œ κ°€μ Έμ˜€κΈ° 였λ₯˜: {e}")
return None
def analyze_news_simple(self, title: str, content: str = "") -> Dict:
"""λ‰΄μŠ€ 기사λ₯Ό μ΄ˆλ“±ν•™μƒ μˆ˜μ€€μœΌλ‘œ 뢄석"""
analysis_templates = {
"μ±—GPT": {
"summary": "λ§ˆμ΄ν¬λ‘œμ†Œν”„νŠΈ(MS)λΌλŠ” 큰 νšŒμ‚¬κ°€ μ±—GPTλΌλŠ” AIλ₯Ό λ„ˆλ¬΄ λ§Žμ€ μ‚¬λžŒλ“€μ΄ μ‚¬μš©ν•΄μ„œ, 컴퓨터λ₯Ό λ³΄κ΄€ν•˜λŠ” 큰 건물(데이터센터)이 λΆ€μ‘±ν•˜λ‹€κ³  λ§ν–ˆμ–΄μš”.",
"significance": "μ±—GPTκ°€ 정말 인기가 λ§Žλ‹€λŠ” λœ»μ΄μ—μš”. 마치 λ„ˆλ¬΄ λ§Žμ€ μΉœκ΅¬λ“€μ΄ ν•œ κ²Œμž„κΈ°λ₯Ό μ“°λ €κ³  ν•˜λŠ” 것과 λΉ„μŠ·ν•΄μš”.",
"impact_level": "high",
"impact_text": "λ†’μŒ",
"impact_description": "AI 기술이 λΉ λ₯΄κ²Œ λ°œμ „ν•˜κ³  있고, λ§Žμ€ μ‚¬λžŒλ“€μ΄ μ‚¬μš©ν•˜κ³  μžˆλ‹€λŠ” μ€‘μš”ν•œ μ‹ ν˜Έμ˜ˆμš”.",
"action": "μ±—GPT 같은 AI 도ꡬλ₯Ό λ°°μ›Œλ³΄μ„Έμš”. μˆ™μ œλ₯Ό 도와달라고 ν•˜κ±°λ‚˜, λͺ¨λ₯΄λŠ” 것을 λ¬Όμ–΄λ³Ό 수 μžˆμ–΄μš”!"
},
"GPU": {
"summary": "미ꡭ이 μ•„λžμ—λ―Έλ¦¬νŠΈ(UAE)λΌλŠ” λ‚˜λΌμ— GPUλΌλŠ” νŠΉλ³„ν•œ 컴퓨터 λΆ€ν’ˆμ„ νŒ” 수 있게 ν—ˆλ½ν–ˆμ–΄μš”. GPUλŠ” AIλ₯Ό λ§Œλ“œλŠ” 데 κΌ­ ν•„μš”ν•œ λΆ€ν’ˆμ΄μ—μš”.",
"significance": "GPUλŠ” AI의 λ‘λ‡Œ 같은 κ±°μ˜ˆμš”. 이걸 νŒ” 수 있게 되면 더 λ§Žμ€ λ‚˜λΌμ—μ„œ AIλ₯Ό λ§Œλ“€ 수 μžˆμ–΄μš”.",
"impact_level": "medium",
"impact_text": "쀑간",
"impact_description": "AI 기술이 더 λ§Žμ€ λ‚˜λΌλ‘œ 퍼질 수 있게 λ˜μ—ˆμ–΄μš”.",
"action": "컴퓨터가 μ–΄λ–»κ²Œ μž‘λ™ν•˜λŠ”μ§€ 관심을 κ°€μ Έλ³΄μ„Έμš”. GPUκ°€ 무엇인지 κ²€μƒ‰ν•΄λ³΄λŠ” 것도 μ’‹μ•„μš”!"
},
"μ†ŒλΌ": {
"summary": "μ˜€ν”ˆAIκ°€ λ§Œλ“  'μ†ŒλΌ'λΌλŠ” AI 앱이 μ—„μ²­ λΉ λ₯΄κ²Œ 인기λ₯Ό μ–»μ—ˆμ–΄μš”. 100만 λͺ…이 λ‹€μš΄λ‘œλ“œν•˜λŠ” 데 μ±—GPT보닀 더 λΉ¨λžλŒ€μš”!",
"significance": "μ‚¬λžŒλ“€μ΄ λΉ„λ””μ˜€λ₯Ό λ§Œλ“œλŠ” AI에 정말 관심이 λ§Žλ‹€λŠ” λœ»μ΄μ—μš”.",
"impact_level": "high",
"impact_text": "λ†’μŒ",
"impact_description": "μ•žμœΌλ‘œ λˆ„κ΅¬λ‚˜ μ‰½κ²Œ λ©‹μ§„ λΉ„λ””μ˜€λ₯Ό λ§Œλ“€ 수 있게 될 κ±°μ˜ˆμš”.",
"action": "μ†ŒλΌλ₯Ό 써보고, μƒμƒν•œ 것을 λΉ„λ””μ˜€λ‘œ λ§Œλ“€μ–΄λ³΄μ„Έμš”. 창의λ ₯을 λ°œνœ˜ν•  수 μžˆμ–΄μš”!"
}
}
# ν‚€μ›Œλ“œ 맀칭으둜 ν…œν”Œλ¦Ώ 선택
for keyword, template in analysis_templates.items():
if keyword.lower() in title.lower():
return template
# κΈ°λ³Έ 뢄석
return {
"summary": f"'{title}'λΌλŠ” AI κ΄€λ ¨ λ‰΄μŠ€κ°€ λ‚˜μ™”μ–΄μš”. AI 기술이 계속 λ°œμ „ν•˜κ³  μžˆλ‹€λŠ” μ†Œμ‹μ΄μ—μš”.",
"significance": "AIλŠ” 우리 μƒν™œμ„ 더 νŽΈλ¦¬ν•˜κ²Œ λ§Œλ“€μ–΄μ£ΌλŠ” κΈ°μˆ μ΄μ—μš”.",
"impact_level": "medium",
"impact_text": "쀑간",
"impact_description": "AI 기술의 λ°œμ „μ€ 우리 λ―Έλž˜μ— μ€‘μš”ν•œ 영ν–₯을 쀄 κ±°μ˜ˆμš”.",
"action": "AI에 λŒ€ν•΄ 더 μ•Œμ•„λ³΄κ³ , AIλ₯Ό ν™œμš©ν•˜λŠ” 방법을 λ°°μ›Œλ³΄μ„Έμš”!"
}
def analyze_model(self, model_name: str, task: str, downloads: int) -> str:
"""ν—ˆκΉ…νŽ˜μ΄μŠ€ λͺ¨λΈ 뢄석 - λͺ¨λΈ μΉ΄λ“œλ₯Ό LLM으둜 뢄석"""
# 1. λͺ¨λΈ μΉ΄λ“œ κ°€μ Έμ˜€κΈ°
model_card = self.fetch_model_card(model_name)
# 2. LLM으둜 뢄석
if model_card and self.api_available:
try:
messages = [
{
"role": "system",
"content": "당신은 μ΄ˆλ“±ν•™μƒλ„ 이해할 수 있게 AI λͺ¨λΈμ„ μ‰½κ²Œ μ„€λͺ…ν•˜λŠ” μ „λ¬Έκ°€μž…λ‹ˆλ‹€. ν•œκ΅­μ–΄λ‘œ λ‹΅λ³€ν•˜μ„Έμš”."
},
{
"role": "user",
"content": f"""λ‹€μŒμ€ ν—ˆκΉ…νŽ˜μ΄μŠ€ λͺ¨λΈ '{model_name}'의 λͺ¨λΈ μΉ΄λ“œμž…λ‹ˆλ‹€:
{model_card}
이 λͺ¨λΈμ„ μ΄ˆλ“±ν•™μƒμ΄ 이해할 수 μžˆλ„λ‘ 3-4λ¬Έμž₯으둜 μ‰½κ²Œ μ„€λͺ…ν•΄μ£Όμ„Έμš”. λ‹€μŒ λ‚΄μš©μ„ ν¬ν•¨ν•˜μ„Έμš”:
1. 이 λͺ¨λΈμ΄ 무엇을 ν•˜λŠ”μ§€
2. μ–΄λ–€ νŠΉμ§•μ΄ μžˆλŠ”μ§€
3. λˆ„κ°€ μ‚¬μš©ν•˜λ©΄ 쒋은지
닡변은 λ°˜λ“œμ‹œ 3-4λ¬Έμž₯의 ν•œκ΅­μ–΄λ‘œλ§Œ μž‘μ„±ν•˜μ„Έμš”."""
}
]
result = self.call_llm(messages, max_tokens=500)
if result:
return result.strip()
except Exception as e:
print(f" ⚠️ λͺ¨λΈ 뢄석 LLM 였λ₯˜: {e}")
# 3. Fallback: ν…œν”Œλ¦Ώ 기반 μ„€λͺ…
task_explanations = {
"text-generation": "글을 μžλ™μœΌλ‘œ λ§Œλ“€μ–΄μ£ΌλŠ”",
"image-to-text": "사진을 보고 μ„€λͺ…을 μ¨μ£ΌλŠ”",
"text-to-image": "글을 읽고 그림을 κ·Έλ €μ£ΌλŠ”",
"translation": "λ‹€λ₯Έ μ–Έμ–΄λ‘œ λ²ˆμ—­ν•΄μ£ΌλŠ”",
"question-answering": "μ§ˆλ¬Έμ— λ‹΅ν•΄μ£ΌλŠ”",
"summarization": "κΈ΄ 글을 짧게 μš”μ•½ν•΄μ£ΌλŠ”",
"text-classification": "글을 λΆ„λ₯˜ν•΄μ£ΌλŠ”",
"token-classification": "단어λ₯Ό λΆ„μ„ν•΄μ£ΌλŠ”",
"fill-mask": "λΉˆμΉΈμ„ μ±„μ›Œμ£ΌλŠ”"
}
task_desc = task_explanations.get(task, "νŠΉλ³„ν•œ κΈ°λŠ₯을 ν•˜λŠ”")
if downloads > 10000000:
popularity = "μ—„μ²­λ‚˜κ²Œ λ§Žμ€"
elif downloads > 1000000:
popularity = "μ•„μ£Ό λ§Žμ€"
elif downloads > 100000:
popularity = "λ§Žμ€"
else:
popularity = "μ–΄λŠ 정도"
return f"이 λͺ¨λΈμ€ {task_desc} AIμ˜ˆμš”. {popularity} μ‚¬λžŒλ“€μ΄ λ‹€μš΄λ‘œλ“œν•΄μ„œ μ‚¬μš©ν•˜κ³  μžˆμ–΄μš”. {model_name.split('/')[-1]}λΌλŠ” μ΄λ¦„μœΌλ‘œ 유λͺ…ν•΄μš”!"
def analyze_space(self, space_name: str, space_id: str, description: str) -> Dict:
"""ν—ˆκΉ…νŽ˜μ΄μŠ€ 슀페이슀 뢄석 - app.pyλ₯Ό LLM으둜 뢄석"""
# 1. app.py μ½”λ“œ κ°€μ Έμ˜€κΈ°
app_code = self.fetch_space_code(space_id)
# 2. LLM으둜 뢄석
if app_code and self.api_available:
try:
messages = [
{
"role": "system",
"content": "당신은 μ΄ˆλ“±ν•™μƒλ„ 이해할 수 있게 AI μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ μ‰½κ²Œ μ„€λͺ…ν•˜λŠ” μ „λ¬Έκ°€μž…λ‹ˆλ‹€. ν•œκ΅­μ–΄λ‘œ λ‹΅λ³€ν•˜μ„Έμš”."
},
{
"role": "user",
"content": f"""λ‹€μŒμ€ ν—ˆκΉ…νŽ˜μ΄μŠ€ 슀페이슀 '{space_name}'의 app.py μ½”λ“œμž…λ‹ˆλ‹€:
{app_code}
이 앱을 μ΄ˆλ“±ν•™μƒμ΄ 이해할 수 μžˆλ„λ‘ 3-4λ¬Έμž₯으둜 μ‰½κ²Œ μ„€λͺ…ν•΄μ£Όμ„Έμš”. λ‹€μŒ λ‚΄μš©μ„ ν¬ν•¨ν•˜μ„Έμš”:
1. 이 앱이 무엇을 ν•˜λŠ”μ§€
2. μ–΄λ–€ κΈ°μˆ μ„ μ‚¬μš©ν•˜λŠ”μ§€
3. μ–΄λ–»κ²Œ ν™œμš©ν•  수 μžˆλŠ”μ§€
닡변은 λ°˜λ“œμ‹œ 3-4λ¬Έμž₯의 ν•œκ΅­μ–΄λ‘œλ§Œ μž‘μ„±ν•˜μ„Έμš”."""
}
]
result = self.call_llm(messages, max_tokens=500)
if result:
# 기술 μŠ€νƒ μΆ”μΆœ μ‹œλ„
tech_stack = []
if 'gradio' in app_code.lower():
tech_stack.append('Gradio')
if 'streamlit' in app_code.lower():
tech_stack.append('Streamlit')
if 'transformers' in app_code.lower():
tech_stack.append('Transformers')
if 'torch' in app_code.lower() or 'pytorch' in app_code.lower():
tech_stack.append('PyTorch')
if 'tensorflow' in app_code.lower():
tech_stack.append('TensorFlow')
if 'diffusers' in app_code.lower():
tech_stack.append('Diffusers')
if not tech_stack:
tech_stack = ['Python', 'AI']
return {
"simple_explanation": result.strip(),
"tech_stack": tech_stack
}
except Exception as e:
print(f" ⚠️ 슀페이슀 뢄석 LLM 였λ₯˜: {e}")
# 3. Fallback: ν…œν”Œλ¦Ώ 기반 μ„€λͺ…
return {
"simple_explanation": f"{space_name}λŠ” μ›ΉλΈŒλΌμš°μ €μ—μ„œ λ°”λ‘œ AIλ₯Ό μ²΄ν—˜ν•΄λ³Ό 수 μžˆλŠ” κ³³μ΄μ—μš”. μ„€μΉ˜ 없이도 μ‚¬μš©ν•  수 μžˆμ–΄μ„œ νŽΈλ¦¬ν•΄μš”! 마치 온라인 κ²Œμž„μ²˜λŸΌ λ°”λ‘œ μ ‘μ†ν•΄μ„œ AIλ₯Ό μ‚¬μš©ν•  수 μžˆλ‹΅λ‹ˆλ‹€.",
"tech_stack": ["Python", "Gradio", "Transformers", "PyTorch"]
}
# ============================================
# κ³ κΈ‰ 뢄석기 클래슀
# ============================================
class AdvancedAIAnalyzer:
"""LLM 기반 κ³ κΈ‰ AI λ‰΄μŠ€ 뢄석기"""
def __init__(self):
self.llm_analyzer = LLMAnalyzer()
self.huggingface_data = {
"models": [],
"spaces": []
}
self.news_data = []
def fetch_aitimes_news(self) -> List[Dict]:
"""AI Timesμ—μ„œ 였늘 λ‚ μ§œ λ‰΄μŠ€ 크둀링"""
print("πŸ“° AI Times λ‰΄μŠ€ μˆ˜μ§‘ 쀑...")
# μˆ˜μ§‘ν•  URL λͺ©λ‘
urls = [
'https://www.aitimes.com/news/articleList.html?sc_multi_code=S2&view_type=sm',
'https://www.aitimes.com/news/articleList.html?sc_section_code=S1N24&view_type=sm'
]
all_news = []
today = datetime.now().strftime('%m-%d') # 예: '10-10'
for url_idx, url in enumerate(urls, 1):
try:
print(f" πŸ” [{url_idx}/2] μˆ˜μ§‘ 쀑: {url}")
response = requests.get(url, timeout=15, headers={
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
})
response.raise_for_status()
response.encoding = 'utf-8'
text = response.text
# νŒ¨ν„΄: [제λͺ©](링크)...λ‚ μ§œ
# 예: [MS "κΈ‰μ¦ν•˜λŠ” 'μ±—GPT' μˆ˜μš”λ‘œ 데이터센터 λΆ€μ‘±...2026λ…„κΉŒμ§€ 지속될 λ“―"](https://www.aitimes.com/news/articleView.html?idxno=203055)
# ...
# μ‚°μ—…μΌλ°˜λ°•μ°¬ 기자10-10 15:10
# 제λͺ©κ³Ό 링크 λ§€μΉ­ νŒ¨ν„΄
pattern = r'\[([^\]]+)\]\((https://www\.aitimes\.com/news/articleView\.html\?idxno=\d+)\)'
matches = re.finditer(pattern, text)
articles_found = 0
for match in matches:
try:
title = match.group(1).strip()
link = match.group(2).strip()
# 제λͺ©μ΄ λ„ˆλ¬΄ 짧으면 μŠ€ν‚΅
if len(title) < 10:
continue
# ν•΄λ‹Ή κΈ°μ‚¬μ˜ λ‚ μ§œ μ°ΎκΈ° (링크 λ’€μ—μ„œ 100자 이내)
pos = match.end()
nearby_text = text[pos:pos+200]
# λ‚ μ§œ νŒ¨ν„΄: 10-10 15:10 ν˜•μ‹
date_pattern = r'(\d{2}-\d{2}\s+\d{2}:\d{2})'
date_match = re.search(date_pattern, nearby_text)
date_text = date_match.group(1) if date_match else today
# 였늘 λ‚ μ§œλ§Œ 필터링
if today not in date_text:
continue
news_item = {
'title': title,
'url': link,
'date': date_text,
'source': 'AI Times',
'category': 'AI'
}
all_news.append(news_item)
articles_found += 1
print(f" βœ“ μΆ”κ°€: {title[:60]}... ({date_text})")
except Exception as e:
continue
print(f" β†’ {articles_found}개 였늘자 기사 발견\n")
time.sleep(1) # μ„œλ²„ λΆ€ν•˜ λ°©μ§€
except Exception as e:
print(f" ⚠️ URL μˆ˜μ§‘ 였λ₯˜: {e}\n")
continue
# 쀑볡 제거 (URL κΈ°μ€€)
unique_news = []
seen_urls = set()
for news in all_news:
if news['url'] not in seen_urls:
unique_news.append(news)
seen_urls.add(news['url'])
print(f"βœ… 총 {len(unique_news)}개 쀑볡 제거된 였늘자 λ‰΄μŠ€\n")
# μ΅œμ†Œ 3κ°œλŠ” 보μž₯ (μ—†μœΌλ©΄ μƒ˜ν”Œ μΆ”κ°€)
if len(unique_news) < 3:
print("⚠️ λ‰΄μŠ€κ°€ λΆ€μ‘±ν•˜μ—¬ 졜근 μƒ˜ν”Œ μΆ”κ°€\n")
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': 'AI'
},
{
'title': 'λ―Έκ΅­, UAE에 GPU 판맀 일뢀 승인...μ—”λΉ„λ””μ•„ μ‹œμ΄ 5μ‘°λ‹¬λŸ¬ λˆˆμ•ž',
'url': 'https://www.aitimes.com/news/articleView.html?idxno=203053',
'date': '10-10 14:46',
'source': 'AI Times',
'category': 'AI'
},
{
'title': 'μ†ŒλΌ, μ±—GPT보닀 빨리 100만 λ‹€μš΄λ‘œλ“œ 돌파',
'url': 'https://www.aitimes.com/news/articleView.html?idxno=203045',
'date': '10-10 12:55',
'source': 'AI Times',
'category': 'AI'
}
]
for sample in sample_news:
if sample['url'] not in seen_urls:
unique_news.append(sample)
return unique_news[:20] # μ΅œλŒ€ 20개
def fetch_huggingface_models(self, limit: int = 30) -> List[Dict]:
"""ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© λͺ¨λΈ 30개 μˆ˜μ§‘ (μ‹€μ œ API)"""
print(f"πŸ€— ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© λͺ¨λΈ {limit}개 μˆ˜μ§‘ 쀑...")
models_list = []
try:
# Hugging Face API μ‚¬μš©
api = HfApi()
# trending μˆœμœ„λ‘œ λͺ¨λΈ κ°€μ Έμ˜€κΈ°
models = list(api.list_models(
sort="trending_score",
direction=-1,
limit=limit
))
print(f"πŸ“Š APIμ—μ„œ {len(models)}개 λͺ¨λΈ λ°›μŒ")
for idx, model in enumerate(models[:limit], 1):
try:
model_info = {
'name': model.id,
'downloads': getattr(model, 'downloads', 0) or 0,
'likes': getattr(model, 'likes', 0) or 0,
'task': getattr(model, 'pipeline_tag', 'N/A') or 'N/A',
'url': f"https://huggingface.co/{model.id}",
'rank': idx
}
# LLM 뢄석 μΆ”κ°€ (λͺ¨λΈ μΉ΄λ“œ 뢄석)
print(f" πŸ” {idx}. {model.id} 뢄석 쀑...")
model_info['analysis'] = self.llm_analyzer.analyze_model(
model_info['name'],
model_info['task'],
model_info['downloads']
)
models_list.append(model_info)
# API rate limit λ°©μ§€λ₯Ό μœ„ν•œ 짧은 λŒ€κΈ°
time.sleep(0.5)
# 진행상황 ν‘œμ‹œ
if idx % 5 == 0:
print(f" βœ“ {idx}개 λͺ¨λΈ 처리 μ™„λ£Œ...")
except Exception as e:
print(f" ⚠️ λͺ¨λΈ {idx} 처리 였λ₯˜: {e}")
continue
print(f"βœ… {len(models_list)}개 νŠΈλ Œλ”© λͺ¨λΈ μˆ˜μ§‘ μ™„λ£Œ")
# DB에 μ €μž₯
if models_list:
save_models_to_db(models_list)
return models_list
except Exception as e:
print(f"❌ λͺ¨λΈ μˆ˜μ§‘ 였λ₯˜: {e}")
print("πŸ’Ύ DBμ—μ„œ 이전 데이터 λ‘œλ“œ μ‹œλ„...")
return load_models_from_db()
def fetch_huggingface_spaces(self, limit: int = 30) -> List[Dict]:
"""ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© 슀페이슀 30개 μˆ˜μ§‘ (μ‹€μ œ API)"""
print(f"πŸš€ ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© 슀페이슀 {limit}개 μˆ˜μ§‘ 쀑...")
spaces_list = []
try:
# Hugging Face API μ‚¬μš©
api = HfApi()
# trending μˆœμœ„λ‘œ 슀페이슀 κ°€μ Έμ˜€κΈ°
spaces = list(api.list_spaces(
sort="trending_score",
direction=-1,
limit=limit
))
print(f"πŸ“Š APIμ—μ„œ {len(spaces)}개 슀페이슀 λ°›μŒ")
for idx, space in enumerate(spaces[:limit], 1):
try:
space_info = {
'space_id': space.id,
'name': space.id.split('/')[-1] if '/' in space.id else space.id,
'author': space.author,
'title': getattr(space, 'title', space.id) or space.id,
'likes': getattr(space, 'likes', 0) or 0,
'url': f"https://huggingface.co/spaces/{space.id}",
'sdk': getattr(space, 'sdk', 'gradio') or 'gradio',
'rank': idx
}
# LLM 뢄석 μΆ”κ°€ (app.py 뢄석)
print(f" πŸ” {idx}. {space.id} 뢄석 쀑...")
space_analysis = self.llm_analyzer.analyze_space(
space_info['name'],
space_info['space_id'],
space_info['title']
)
space_info['simple_explanation'] = space_analysis['simple_explanation']
space_info['tech_stack'] = space_analysis['tech_stack']
space_info['description'] = space_info['title']
spaces_list.append(space_info)
# API rate limit λ°©μ§€λ₯Ό μœ„ν•œ 짧은 λŒ€κΈ°
time.sleep(0.5)
# 진행상황 ν‘œμ‹œ
if idx % 5 == 0:
print(f" βœ“ {idx}개 슀페이슀 처리 μ™„λ£Œ...")
except Exception as e:
print(f" ⚠️ 슀페이슀 {idx} 처리 였λ₯˜: {e}")
continue
print(f"βœ… {len(spaces_list)}개 νŠΈλ Œλ”© 슀페이슀 μˆ˜μ§‘ μ™„λ£Œ")
# DB에 μ €μž₯
if spaces_list:
save_spaces_to_db(spaces_list)
return spaces_list
except Exception as e:
print(f"❌ 슀페이슀 μˆ˜μ§‘ 였λ₯˜: {e}")
print("πŸ’Ύ DBμ—μ„œ 이전 데이터 λ‘œλ“œ μ‹œλ„...")
return load_spaces_from_db()
def analyze_all_news(self) -> List[Dict]:
"""λͺ¨λ“  λ‰΄μŠ€μ— LLM 뢄석 μΆ”κ°€"""
print("πŸ“° λ‰΄μŠ€ LLM 뢄석 μ‹œμž‘...")
# μ‹€μ œ μ›Ήμ‚¬μ΄νŠΈμ—μ„œ λ‰΄μŠ€ μˆ˜μ§‘
news = self.fetch_aitimes_news()
if not news:
print("⚠️ μˆ˜μ§‘λœ λ‰΄μŠ€κ°€ μ—†μŠ΅λ‹ˆλ‹€.")
return []
analyzed_news = []
for idx, article in enumerate(news, 1):
print(f" 🧠 {idx}/{len(news)}: {article['title'][:50]}... 뢄석 쀑")
analysis = self.llm_analyzer.analyze_news_simple(
article['title'],
""
)
article['analysis'] = analysis
analyzed_news.append(article)
print(f"βœ… {len(analyzed_news)}개 λ‰΄μŠ€ 뢄석 μ™„λ£Œ")
# DB에 μ €μž₯
if analyzed_news:
save_news_to_db(analyzed_news)
return analyzed_news
def get_all_data(self, force_refresh: bool = False) -> Dict:
"""λͺ¨λ“  데이터 μˆ˜μ§‘ 및 뢄석
Args:
force_refresh: Trueλ©΄ μƒˆλ‘œ μˆ˜μ§‘, Falseλ©΄ DBμ—μ„œ λ‘œλ“œ ν›„ μ—†μœΌλ©΄ μˆ˜μ§‘
"""
print("\n" + "="*60)
print("πŸš€ AI λ‰΄μŠ€ & ν—ˆκΉ…νŽ˜μ΄μŠ€ LLM 뢄석 μ‹œμž‘")
print("="*60 + "\n")
if force_refresh:
print("πŸ”„ κ°•μ œ μƒˆλ‘œκ³ μΉ¨ λͺ¨λ“œ: λͺ¨λ“  데이터 μƒˆλ‘œ μˆ˜μ§‘")
analyzed_news = self.analyze_all_news()
analyzed_models = self.fetch_huggingface_models(30)
analyzed_spaces = self.fetch_huggingface_spaces(30)
else:
print("πŸ’Ύ DB μš°μ„  λ‘œλ“œ λͺ¨λ“œ")
# DBμ—μ„œ λ¨Όμ € λ‘œλ“œ
analyzed_news = load_news_from_db()
if not analyzed_news:
print("πŸ“° DB에 λ‰΄μŠ€ μ—†μŒ β†’ μƒˆλ‘œ μˆ˜μ§‘")
analyzed_news = self.analyze_all_news()
else:
print(f"βœ… DBμ—μ„œ {len(analyzed_news)}개 λ‰΄μŠ€ λ‘œλ“œ")
analyzed_models = load_models_from_db()
if not analyzed_models:
print("πŸ€— DB에 λͺ¨λΈ μ—†μŒ β†’ μƒˆλ‘œ μˆ˜μ§‘")
analyzed_models = self.fetch_huggingface_models(30)
else:
print(f"βœ… DBμ—μ„œ {len(analyzed_models)}개 λͺ¨λΈ λ‘œλ“œ")
analyzed_spaces = load_spaces_from_db()
if not analyzed_spaces:
print("πŸš€ DB에 슀페이슀 μ—†μŒ β†’ μƒˆλ‘œ μˆ˜μ§‘")
analyzed_spaces = self.fetch_huggingface_spaces(30)
else:
print(f"βœ… DBμ—μ„œ {len(analyzed_spaces)}개 슀페이슀 λ‘œλ“œ")
# 톡계
stats = {
'total_news': len(analyzed_news),
'hf_models': len(analyzed_models),
'hf_spaces': len(analyzed_spaces),
'llm_analyses': len(analyzed_news) + len(analyzed_models) + len(analyzed_spaces)
}
print(f"\nβœ… 전체 뢄석 μ™„λ£Œ: {stats['llm_analyses']}개 ν•­λͺ©")
print(f" πŸ“° λ‰΄μŠ€: {stats['total_news']}개")
print(f" πŸ€— λͺ¨λΈ: {stats['hf_models']}개")
print(f" πŸš€ 슀페이슀: {stats['hf_spaces']}개")
return {
'analyzed_news': analyzed_news,
'analyzed_models': analyzed_models,
'analyzed_spaces': analyzed_spaces,
'stats': stats,
'timestamp': datetime.now().strftime('%Yλ…„ %mμ›” %d일 %H:%M:%S')
}
# ============================================
# Flask 라우트
# ============================================
@app.route('/')
def index():
"""메인 νŽ˜μ΄μ§€"""
try:
# refresh νŒŒλΌλ―Έν„° 확인
force_refresh = request.args.get('refresh', 'false').lower() == 'true'
analyzer = AdvancedAIAnalyzer()
data = analyzer.get_all_data(force_refresh=force_refresh)
return render_template_string(HTML_TEMPLATE, **data)
except Exception as e:
import traceback
error_detail = traceback.format_exc()
return f"""
<html>
<body style="font-family: Arial; padding: 50px; text-align: center;">
<h1 style="color: #e74c3c;">⚠️ 였λ₯˜ λ°œμƒ</h1>
<p>{str(e)}</p>
<pre style="text-align: left; background: #f5f5f5; padding: 20px; border-radius: 5px;">
{error_detail}
</pre>
<button onclick="location.href='/'" style="padding: 10px 20px; margin: 10px;">
πŸ”„ μƒˆλ‘œκ³ μΉ¨
</button>
<button onclick="location.href='/?refresh=true'" style="padding: 10px 20px; margin: 10px; background: #ff6b6b; color: white; border: none; border-radius: 5px;">
πŸ”₯ κ°•μ œ κ°±μ‹ 
</button>
</body>
</html>
""", 500
@app.route('/api/data')
def api_data():
"""JSON API"""
try:
force_refresh = request.args.get('refresh', 'false').lower() == 'true'
analyzer = AdvancedAIAnalyzer()
data = analyzer.get_all_data(force_refresh=force_refresh)
return jsonify({
'success': True,
'data': data
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
@app.route('/api/refresh')
def api_refresh():
"""κ°•μ œ μƒˆλ‘œκ³ μΉ¨ API"""
try:
analyzer = AdvancedAIAnalyzer()
data = analyzer.get_all_data(force_refresh=True)
return jsonify({
'success': True,
'message': '데이터가 μ„±κ³΅μ μœΌλ‘œ κ°±μ‹ λ˜μ—ˆμŠ΅λ‹ˆλ‹€',
'stats': data['stats']
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
@app.route('/health')
def health():
"""ν—¬μŠ€ 체크"""
try:
# DB μ—°κ²° 확인
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM news")
news_count = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM models")
models_count = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM spaces")
spaces_count = cursor.fetchone()[0]
conn.close()
return jsonify({
"status": "healthy",
"service": "AI News LLM Analyzer",
"version": "3.2.0",
"database": {
"connected": True,
"news_count": news_count,
"models_count": models_count,
"spaces_count": spaces_count
},
"fireworks_api": {
"configured": bool(os.environ.get('FIREWORKS_API_KEY'))
},
"timestamp": datetime.now().isoformat()
})
except Exception as e:
return jsonify({
"status": "unhealthy",
"error": str(e)
}), 500
# ============================================
# 메인 μ‹€ν–‰
# ============================================
if __name__ == '__main__':
port = int(os.environ.get('PORT', 7860))
print(f"""
╔════════════════════════════════════════════════════════════╗
β•‘ β•‘
β•‘ πŸ€– AI λ‰΄μŠ€ & ν—ˆκΉ…νŽ˜μ΄μŠ€ LLM 뢄석 μ›Ήμ•± v3.2 β•‘
β•‘ β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
✨ μ£Όμš” κΈ°λŠ₯:
β€’ πŸ’Ύ SQLite DB 영ꡬ μŠ€ν† λ¦¬μ§€
β€’ 🌐 AI Times μ‹€μ‹œκ°„ λ‰΄μŠ€ 크둀링 (2개 μ„Ήμ…˜)
β€’ πŸ“° λ‰΄μŠ€ μ΄ˆλ“±ν•™μƒ μˆ˜μ€€ 뢄석
β€’ πŸ€— ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© λͺ¨λΈ TOP 30 (λͺ¨λΈ μΉ΄λ“œ 뢄석)
β€’ πŸš€ ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© 슀페이슀 TOP 30 (app.py 뢄석)
β€’ 🧠 Fireworks AI (Qwen3-235B) μ‹€μ‹œκ°„ LLM 뢄석
β€’ 🎨 νƒ­ UI (λ‰΄μŠ€/λͺ¨λΈ/슀페이슀)
πŸ”‘ API μ„€μ •:
FIREWORKS_API_KEY: {"βœ… 섀정됨" if os.environ.get('FIREWORKS_API_KEY') else "❌ λ―Έμ„€μ • (ν…œν”Œλ¦Ώ λͺ¨λ“œ)"}
πŸš€ μ„œλ²„ 정보:
πŸ“ 메인: http://localhost:{port}
πŸ”„ κ°•μ œκ°±μ‹ : http://localhost:{port}/?refresh=true
πŸ“Š API: http://localhost:{port}/api/data
πŸ”₯ μƒˆλ‘œκ³ μΉ¨ API: http://localhost:{port}/api/refresh
πŸ’š Health: http://localhost:{port}/health
πŸ’Ύ λ°μ΄ν„°λ² μ΄μŠ€: {DB_PATH}
μ΄ˆκΈ°ν™” 쀑...
""")
# λ°μ΄ν„°λ² μ΄μŠ€ μ΄ˆκΈ°ν™”
try:
init_database()
except Exception as e:
print(f"❌ DB μ΄ˆκΈ°ν™” 였λ₯˜: {e}")
sys.exit(1)
print("\nβœ… μ„œλ²„ μ€€λΉ„ μ™„λ£Œ!")
print("λΈŒλΌμš°μ €μ—μ„œ μœ„ URL을 μ—΄μ–΄μ£Όμ„Έμš”!")
print("μ’…λ£Œ: Ctrl+C\n")
try:
app.run(
host='0.0.0.0',
port=port,
debug=False,
threaded=True
)
except KeyboardInterrupt:
print("\n\nπŸ‘‹ μ„œλ²„ μ’…λ£Œ!")
sys.exit(0)
except Exception as e:
print(f"\nβŒμ„œλ²„ 였λ₯˜: {e}")
sys.exit(1)