|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
app = Flask(__name__) |
|
|
app.config['JSON_AS_ASCII'] = False |
|
|
|
|
|
|
|
|
DB_PATH = 'ai_news_analysis.db' |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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] |
|
|
}) |
|
|
|
|
|
conn.close() |
|
|
return spaces_list |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
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μ νλ°μ μΈ μ¬μ©λ μ¦κ°λ‘ μΈν΄ λ°μ΄ν°μΌν° μ©λμ΄ λΆμ‘±ν μν©μ μ§λ©΄νμ΅λλ€. νμ¬ λ―Έκ΅ λ΄ μ¬λ¬ μ§μμμ 물리μ 곡κ°κ³Ό μλ²κ° λͺ¨λ λΆμ‘±ν μνμ΄λ©°, μ΄λ‘ μΈν΄ λ²μ§λμμ ν
μ¬μ€ λ± ν΅μ¬ μ§μμμλ 2026λ
μλ°κΈ°κΉμ§ μ κ· Azure ν΄λΌμ°λ ꡬλ
μ΄ μ νλ κ²μΌλ‘ μμλ©λλ€. μ΄λ μμ±ν AI μλΉμ€μ κΈκ²©ν μ±μ₯μ΄ κ°μ Έμ¨ μΈνλΌ κ³΅κΈ λ¬Έμ λ₯Ό μ¬μ€ν 보μ¬μ£Όλ μ¬λ‘μ
λλ€.", |
|
|
"significance": "μ΄ λ΄μ€λ AI κΈ°μ μ λμ€ν μλκ° κΈ°μ
λ€μ μμμ ν¨μ¬ λ°μ΄λκ³ μμμ 보μ¬μ€λλ€. MS κ°μ κΈλ‘λ² IT κΈ°μ
λ AI μμλ₯Ό λ°λΌμ‘κΈ° μν΄ κ³ κ΅°λΆν¬νκ³ μμΌλ©°, μ΄λ AIκ° λ¨μν μ νμ΄ μλ μ°μ
μ λ°μ λ³νμν€λ ν΅μ¬ κΈ°μ μμ μ¦λͺ
ν©λλ€.", |
|
|
"impact_level": "high", |
|
|
"impact_text": "λμ", |
|
|
"impact_description": "ν΄λΌμ°λ μΈνλΌ λΆμ‘±μ AI μλΉμ€ νμ₯μ μ§μ μ μΈ μν₯μ λ―ΈμΉλ©°, ν₯ν AI κΈ°μ μ κ·Όμ±κ³Ό λΉμ© ꡬ쑰λ₯Ό λ³νμν¬ μ μμ΅λλ€.", |
|
|
"action": "μ±GPTλ Claude κ°μ AI λꡬλ₯Ό νμ©ν νμ΅ λ°©λ²μ μ΅νμΈμ. λ³΄κ³ μ μμ±, μ½λ© νμ΅, μΈκ΅μ΄ κ³΅λΆ λ± λ€μν λΆμΌμμ AIλ₯Ό νμ΅ λ³΄μ‘° λκ΅¬λ‘ μ¬μ©ν μ μμ΅λλ€." |
|
|
}, |
|
|
"GPU": { |
|
|
"summary": "λ―Έκ΅ μ λΆκ° μλμ미리νΈ(UAE)μ μ΅μ²¨λ¨ AI μΉ©(GPU) μμΆμ μΉμΈνμ΅λλ€. μ΄λ² μΉμΈμ UAE λ΄ λ―Έκ΅ κΈ°μ
μ΄ μ΄μνλ λ°μ΄ν°μΌν°μ νμ λλ©°, μ€νAI μ μ© 5GW κ·λͺ¨ λ°μ΄ν°μΌν° ꡬμΆμ μ¬μ©λ μμ μ
λλ€. GPUλ AI λͺ¨λΈ νμ΅μ νμμ μΈ νλμ¨μ΄λ‘, μλΉλμκ° μμ₯μ μ£Όλνκ³ μμΌλ©° μ΄λ² κ²°μ μΌλ‘ μλΉλμμ μκ°μ΄μ‘μ΄ 5μ‘° λ¬λ¬μ κ·Όμ ν κ²μΌλ‘ μ λ§λ©λλ€.", |
|
|
"significance": "μ΄λ λ―Έκ΅μ AI κΈ°μ μμΆ μ μ±
λ³νλ₯Ό 보μ¬μ£Όλ μ€μν μ νΈμ
λλ€. κΈ°μ ν¨κΆ κ²½μ μμμλ μ λ΅μ λλ§Ήκ΅κ³Όμ νλ ₯μ ν΅ν΄ AI μνκ³λ₯Ό νμ₯νλ €λ λ―Έκ΅μ μλλ₯Ό μΏλ³Ό μ μμ΅λλ€.", |
|
|
"impact_level": "medium", |
|
|
"impact_text": "μ€κ°", |
|
|
"impact_description": "AI νλμ¨μ΄ 곡κΈλ§μ μ§μ νμ λ³νλ κΈλ‘λ² AI μ°μ
μ§νλμ μν₯μ λ―ΈμΉ μ μμΌλ©°, νΉν λ°λ체 μ°μ
κ³Ό κ΅μ κ΄κ³μ μ€μν μλ―Έλ₯Ό κ°μ§λλ€.", |
|
|
"action": "μ»΄ν¨ν° νλμ¨μ΄, νΉν GPUμ μλ μ리μ AI νμ΅μμμ μν μ 곡λΆν΄λ³΄μΈμ. λ³λ ¬ μ²λ¦¬, νλ ¬ μ°μ° λ±μ κ°λ
μ μ΄ν΄νλ©΄ AI κΈ°μ μ κ·Όκ°μ νμ
ν μ μμ΅λλ€." |
|
|
}, |
|
|
"μλΌ": { |
|
|
"summary": "μ€νAIμ AI λμμ μμ± μ± 'μλΌ(Sora)'κ° μΆμ 5μΌ λ§μ 100λ§ λ€μ΄λ‘λλ₯Ό λννμ΅λλ€. μ΄λ μ±GPTλ³΄λ€ λΉ λ₯Έ μ±μ₯ μλμ΄λ©°, μ΄λ μ μ©(invite-only) μ±μμ κ³ λ €νλ©΄ λμ± λλΌμ΄ κΈ°λ‘μ
λλ€. μλΌλ ν
μ€νΈ ν둬ννΈλ§μΌλ‘ κ³ νμ§ λμμμ μμ±ν μ μλ μμ±ν AI λꡬλ‘, λ―Έκ΅κ³Ό μΊλλ€μμ iOS μ μ©μΌλ‘ μΆμλμμ΅λλ€.", |
|
|
"significance": "ν
μ€νΈλ₯Ό μ΄λ―Έμ§λ‘ λ³ννλ κΈ°μ μμ λ λμκ° λμμ μμ±κΉμ§ κ°λ₯ν΄μ§ κ²μ AI κΈ°μ μ μ§νλ₯Ό 보μ¬μ€λλ€. μ½ν
μΈ μ μμ λ―Όμ£Όνκ° κ°μνλκ³ μμΌλ©°, λꡬλ μ½κ² κ³ νμ§ μμμ λ§λ€ μ μλ μλκ° μ΄λ¦¬κ³ μμ΅λλ€.", |
|
|
"impact_level": "high", |
|
|
"impact_text": "λμ", |
|
|
"impact_description": "μμ μ μ μ°μ
μ ν¨λ¬λ€μμ΄ λ³ννκ³ μμΌλ©°, κ΅μ‘, λ§μΌν
, μν°ν
μΈλ¨ΌνΈ λ± λ€μν λΆμΌμμ AI λμμ μμ± κΈ°μ μ νμ©μ΄ μ¦κ°ν κ²μΌλ‘ μμλ©λλ€.", |
|
|
"action": "AI λμμ μμ± λꡬμ κ°λ₯μ±κ³Ό νκ³λ₯Ό νꡬν΄λ³΄μΈμ. μ°½μμ μΈ μμ΄λμ΄λ₯Ό μκ°ννλ λ°©λ²μ λ°°μ°κ³ , λμμ λ₯νμ΄ν¬ κ°μ μ
μ© μ¬λ‘μ λν λΉνμ μ¬κ³ λ ν¨μνμΈμ." |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
for keyword, template in analysis_templates.items(): |
|
|
if keyword.lower() in title.lower(): |
|
|
return template |
|
|
|
|
|
|
|
|
return { |
|
|
"summary": f"'{title}'μ κ΄λ ¨λ μ΅μ AI κΈ°μ λν₯μ
λλ€. μΈκ³΅μ§λ₯ λΆμΌλ λΉ λ₯΄κ² λ°μ νκ³ μμΌλ©°, μ΄λ¬ν κΈ°μ λ³νλ μ°λ¦¬μ μΌμμνκ³Ό λ―Έλ μ§μ
μΈκ³μ ν° μν₯μ λ―ΈμΉ κ²μΌλ‘ μμλ©λλ€. κ΄λ ¨ κΈ°μ μ μ리μ μ¬νμ νκΈν¨κ³Όλ₯Ό ν¨κ» μ΄ν΄νλ κ²μ΄ μ€μν©λλ€.", |
|
|
"significance": "AI κΈ°μ μ λ°μ μ λ¨μν κΈ°μ νμ μ λμ΄ μ¬ν, κ²½μ , μ€λ¦¬μ μΈ‘λ©΄μμ λ€μν λ
Όμλ₯Ό λΆλ¬μΌμΌν€κ³ μμ΅λλ€. μ΄λ¬ν λ³νλ₯Ό μ΄ν΄νκ³ λλΉνλ κ²μ΄ λ―Έλ μΈλμκ² μ€μν μλμ
λλ€.", |
|
|
"impact_level": "medium", |
|
|
"impact_text": "μ€κ°", |
|
|
"impact_description": "AI κΈ°μ μ λ°μ μ κ΅μ‘, μ·¨μ
, μ°μ
μ λ°μ κ±Έμ³ κ΅¬μ‘°μ λ³νλ₯Ό κ°μ Έμ¬ κ²μ΄λ©°, μ΄μ λν μ΄ν΄μ μ€λΉκ° νμν©λλ€.", |
|
|
"action": "AI κΈ°μ μ κΈ°λ³Έ μ리λ₯Ό νμ΅νκ³ , κ΄λ ¨ νλ‘κ·Έλλ°(Python λ±)μ΄λ λ°μ΄ν° κ³Όν κΈ°μ΄λ₯Ό 곡λΆν΄λ³΄μΈμ. λν AI μ€λ¦¬μ μ¬νμ μν₯μ λν΄μλ λΉνμ μΌλ‘ μ¬κ³ νλ μ΅κ΄μ κΈ°λ₯΄μΈμ." |
|
|
} |
|
|
|
|
|
def analyze_model(self, model_name: str, task: str, downloads: int) -> str: |
|
|
"""νκΉ
νμ΄μ€ λͺ¨λΈ λΆμ - λͺ¨λΈ μΉ΄λλ₯Ό LLMμΌλ‘ λΆμ""" |
|
|
|
|
|
|
|
|
model_card = self.fetch_model_card(model_name) |
|
|
|
|
|
|
|
|
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}") |
|
|
|
|
|
|
|
|
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μΌλ‘ λΆμ""" |
|
|
|
|
|
|
|
|
app_code = self.fetch_space_code(space_id) |
|
|
|
|
|
|
|
|
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}") |
|
|
|
|
|
|
|
|
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 λ΄μ€ μμ§ μ€...") |
|
|
|
|
|
|
|
|
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') |
|
|
|
|
|
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' |
|
|
|
|
|
soup = BeautifulSoup(response.text, 'html.parser') |
|
|
|
|
|
|
|
|
articles = soup.find_all('a', href=re.compile(r'/news/articleView\.html\?idxno=\d+')) |
|
|
|
|
|
print(f" β {len(articles)}κ° λ§ν¬ λ°κ²¬") |
|
|
|
|
|
articles_found = 0 |
|
|
for article_tag in articles: |
|
|
try: |
|
|
|
|
|
title = article_tag.get_text(strip=True) |
|
|
link = article_tag.get('href', '') |
|
|
|
|
|
|
|
|
if link and not link.startswith('http'): |
|
|
if link.startswith('/'): |
|
|
link = 'https://www.aitimes.com' + link |
|
|
else: |
|
|
link = 'https://www.aitimes.com/' + link |
|
|
|
|
|
|
|
|
if not title or len(title) < 10: |
|
|
continue |
|
|
|
|
|
|
|
|
parent = article_tag.parent |
|
|
date_text = '' |
|
|
|
|
|
|
|
|
if parent: |
|
|
parent_text = parent.get_text() |
|
|
date_match = re.search(r'(\d{2}-\d{2}\s+\d{2}:\d{2})', parent_text) |
|
|
if date_match: |
|
|
date_text = date_match.group(1) |
|
|
|
|
|
|
|
|
if not date_text: |
|
|
for sibling in article_tag.find_next_siblings(): |
|
|
sibling_text = sibling.get_text() |
|
|
date_match = re.search(r'(\d{2}-\d{2}\s+\d{2}:\d{2})', sibling_text) |
|
|
if date_match: |
|
|
date_text = date_match.group(1) |
|
|
break |
|
|
|
|
|
|
|
|
if not date_text: |
|
|
date_text = 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 |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
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] |
|
|
|
|
|
def fetch_huggingface_models(self, limit: int = 30) -> List[Dict]: |
|
|
"""νκΉ
νμ΄μ€ νΈλ λ© λͺ¨λΈ 30κ° μμ§ (μ€μ API)""" |
|
|
print(f"π€ νκΉ
νμ΄μ€ νΈλ λ© λͺ¨λΈ {limit}κ° μμ§ μ€...") |
|
|
|
|
|
models_list = [] |
|
|
|
|
|
try: |
|
|
|
|
|
api = HfApi() |
|
|
|
|
|
|
|
|
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 |
|
|
} |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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)}κ° νΈλ λ© λͺ¨λΈ μμ§ μλ£") |
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
api = HfApi() |
|
|
|
|
|
|
|
|
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 |
|
|
} |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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)}κ° νΈλ λ© μ€νμ΄μ€ μμ§ μλ£") |
|
|
|
|
|
|
|
|
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)}κ° λ΄μ€ λΆμ μλ£") |
|
|
|
|
|
|
|
|
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 μ°μ λ‘λ λͺ¨λ") |
|
|
|
|
|
|
|
|
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') |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/') |
|
|
def index(): |
|
|
"""λ©μΈ νμ΄μ§""" |
|
|
try: |
|
|
|
|
|
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: |
|
|
|
|
|
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) |