Today / app-backup2.py
ginipick's picture
Update app-backup2.py
b330fb2 verified
raw
history blame
66.7 kB
# -*- coding: utf-8 -*-
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의 폭발적인 μ‚¬μš©λŸ‰ μ¦κ°€λ‘œ 인해 데이터센터 μš©λŸ‰μ΄ λΆ€μ‘±ν•œ 상황에 μ§λ©΄ν–ˆμŠ΅λ‹ˆλ‹€. ν˜„μž¬ λ―Έκ΅­ λ‚΄ μ—¬λŸ¬ μ§€μ—­μ—μ„œ 물리적 곡간과 μ„œλ²„κ°€ λͺ¨λ‘ λΆ€μ‘±ν•œ μƒνƒœμ΄λ©°, 이둜 인해 λ²„μ§€λ‹ˆμ•„μ™€ ν…μ‚¬μŠ€ λ“± 핡심 μ§€μ—­μ—μ„œλŠ” 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으둜 뢄석"""
# 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'
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
# 쀑볡 제거 (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)