ginipick commited on
Commit
7352b74
Β·
verified Β·
1 Parent(s): 670f3b8

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +1855 -0
app.py ADDED
@@ -0,0 +1,1855 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ AI λ‰΄μŠ€ & ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© LLM 뢄석 μ›Ήμ•± (μ™„μ „νŒ v3.2)
4
+ 파일λͺ…: app_advanced.py
5
+
6
+ μ£Όμš” κΈ°λŠ₯:
7
+ 1. SQLite DB 영ꡬ μŠ€ν† λ¦¬μ§€
8
+ 2. AI Times μ‹€μ‹œκ°„ λ‰΄μŠ€ 크둀링 (2개 μ„Ήμ…˜)
9
+ 3. μ‹€μ œ Hugging Face Trending API 연동 (λͺ¨λΈ/슀페이슀 30μœ„)
10
+ 4. Fireworks AI (Qwen3-235B) μ‹€μ‹œκ°„ LLM 뢄석
11
+ - λ‰΄μŠ€ μ΄ˆλ“±ν•™μƒ μˆ˜μ€€ 뢄석
12
+ - λͺ¨λΈ μΉ΄λ“œ μžλ™ 뢄석 (README.md)
13
+ - 슀페이슀 μ½”λ“œ μžλ™ 뢄석 (app.py)
14
+ 5. νƒ­ UI (λ‰΄μŠ€/λͺ¨λΈ/슀페이슀)
15
+
16
+ μ‹€ν–‰ 방법:
17
+ 1. pip install Flask requests beautifulsoup4 huggingface_hub
18
+ 2. export FIREWORKS_API_KEY="your-api-key-here" # 선택사항 (μ—†μœΌλ©΄ ν…œν”Œλ¦Ώ λͺ¨λ“œ)
19
+ 3. python app_advanced.py
20
+ 4. λΈŒλΌμš°μ €μ—μ„œ http://localhost:7860 접속
21
+
22
+ ν™˜κ²½λ³€μˆ˜:
23
+ - FIREWORKS_API_KEY: Fireworks AI API ν‚€ (선택, 더 λ‚˜μ€ 뢄석)
24
+ - PORT: μ„œλ²„ 포트 (κΈ°λ³Έκ°’: 7860)
25
+ """
26
+
27
+ from flask import Flask, render_template_string, jsonify, request
28
+ import requests
29
+ import json
30
+ from datetime import datetime
31
+ from typing import List, Dict, Optional
32
+ import os
33
+ import sys
34
+ import sqlite3
35
+ import time
36
+ from huggingface_hub import HfApi
37
+ from bs4 import BeautifulSoup
38
+
39
+ # Flask μ•± μ΄ˆκΈ°ν™”
40
+ app = Flask(__name__)
41
+ app.config['JSON_AS_ASCII'] = False
42
+
43
+ # λ°μ΄ν„°λ² μ΄μŠ€ 파일 경둜
44
+ DB_PATH = 'ai_news_analysis.db'
45
+
46
+
47
+ # ============================================
48
+ # HTML ν…œν”Œλ¦Ώ (νƒ­ UI 포함)
49
+ # ============================================
50
+
51
+ HTML_TEMPLATE = """
52
+ <!DOCTYPE html>
53
+ <html lang="ko">
54
+ <head>
55
+ <meta charset="UTF-8">
56
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
57
+ <title>AI λ‰΄μŠ€ & ν—ˆκΉ…νŽ˜μ΄μŠ€ LLM 뢄석 μ‹œμŠ€ν…œ</title>
58
+ <style>
59
+ * {
60
+ margin: 0;
61
+ padding: 0;
62
+ box-sizing: border-box;
63
+ }
64
+
65
+ body {
66
+ font-family: 'Segoe UI', 'Apple SD Gothic Neo', 'Malgun Gothic', sans-serif;
67
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
68
+ padding: 20px;
69
+ color: #333;
70
+ min-height: 100vh;
71
+ }
72
+
73
+ .container {
74
+ max-width: 1400px;
75
+ margin: 0 auto;
76
+ background: white;
77
+ border-radius: 20px;
78
+ padding: 40px;
79
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
80
+ }
81
+
82
+ h1 {
83
+ text-align: center;
84
+ color: #667eea;
85
+ margin-bottom: 10px;
86
+ font-size: 2.8em;
87
+ font-weight: 800;
88
+ }
89
+
90
+ .subtitle {
91
+ text-align: center;
92
+ color: #666;
93
+ margin-bottom: 40px;
94
+ font-size: 1.2em;
95
+ }
96
+
97
+ /* νƒ­ μŠ€νƒ€μΌ */
98
+ .tabs {
99
+ display: flex;
100
+ gap: 15px;
101
+ margin-bottom: 30px;
102
+ border-bottom: 3px solid #e0e0e0;
103
+ padding-bottom: 0;
104
+ }
105
+
106
+ .tab {
107
+ padding: 15px 30px;
108
+ background: #f5f5f5;
109
+ border: none;
110
+ border-radius: 10px 10px 0 0;
111
+ cursor: pointer;
112
+ font-size: 1.1em;
113
+ font-weight: 600;
114
+ color: #666;
115
+ transition: all 0.3s;
116
+ }
117
+
118
+ .tab.active {
119
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
120
+ color: white;
121
+ transform: translateY(-3px);
122
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
123
+ }
124
+
125
+ .tab:hover {
126
+ background: #e0e0e0;
127
+ }
128
+
129
+ .tab.active:hover {
130
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
131
+ }
132
+
133
+ .tab-content {
134
+ display: none;
135
+ }
136
+
137
+ .tab-content.active {
138
+ display: block;
139
+ animation: fadeIn 0.5s ease-out;
140
+ }
141
+
142
+ /* 톡계 μΉ΄λ“œ */
143
+ .stats {
144
+ display: grid;
145
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
146
+ gap: 25px;
147
+ margin-bottom: 50px;
148
+ }
149
+
150
+ .stat-card {
151
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
152
+ color: white;
153
+ padding: 30px;
154
+ border-radius: 15px;
155
+ text-align: center;
156
+ box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
157
+ transform: translateY(0);
158
+ transition: transform 0.3s, box-shadow 0.3s;
159
+ }
160
+
161
+ .stat-card:hover {
162
+ transform: translateY(-5px);
163
+ box-shadow: 0 12px 30px rgba(102, 126, 234, 0.6);
164
+ }
165
+
166
+ .stat-number {
167
+ font-size: 3.5em;
168
+ font-weight: bold;
169
+ margin-bottom: 10px;
170
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
171
+ }
172
+
173
+ .stat-label {
174
+ font-size: 1.2em;
175
+ opacity: 0.95;
176
+ font-weight: 500;
177
+ }
178
+
179
+ /* λ‰΄μŠ€ μΉ΄λ“œ (LLM 뢄석 버전) */
180
+ .news-card {
181
+ background: white;
182
+ border-radius: 15px;
183
+ padding: 30px;
184
+ margin-bottom: 25px;
185
+ box-shadow: 0 5px 20px rgba(0,0,0,0.1);
186
+ border-left: 6px solid #667eea;
187
+ transition: all 0.3s;
188
+ }
189
+
190
+ .news-card:hover {
191
+ transform: translateX(10px);
192
+ box-shadow: 0 10px 30px rgba(0,0,0,0.15);
193
+ }
194
+
195
+ .news-header {
196
+ display: flex;
197
+ justify-content: space-between;
198
+ align-items: flex-start;
199
+ margin-bottom: 20px;
200
+ flex-wrap: wrap;
201
+ gap: 15px;
202
+ }
203
+
204
+ .news-title {
205
+ font-size: 1.4em;
206
+ font-weight: 700;
207
+ color: #2c3e50;
208
+ flex: 1;
209
+ min-width: 300px;
210
+ }
211
+
212
+ .news-meta {
213
+ display: flex;
214
+ gap: 15px;
215
+ color: #7f8c8d;
216
+ font-size: 0.9em;
217
+ }
218
+
219
+ .analysis-section {
220
+ background: #f8f9fa;
221
+ padding: 20px;
222
+ border-radius: 10px;
223
+ margin-top: 15px;
224
+ }
225
+
226
+ .analysis-item {
227
+ margin-bottom: 20px;
228
+ padding-bottom: 20px;
229
+ border-bottom: 1px solid #e0e0e0;
230
+ }
231
+
232
+ .analysis-item:last-child {
233
+ border-bottom: none;
234
+ margin-bottom: 0;
235
+ padding-bottom: 0;
236
+ }
237
+
238
+ .analysis-label {
239
+ display: inline-block;
240
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
241
+ color: white;
242
+ padding: 8px 15px;
243
+ border-radius: 20px;
244
+ font-size: 0.9em;
245
+ font-weight: 600;
246
+ margin-bottom: 10px;
247
+ }
248
+
249
+ .analysis-content {
250
+ color: #34495e;
251
+ line-height: 1.8;
252
+ font-size: 1.05em;
253
+ }
254
+
255
+ .impact-level {
256
+ display: inline-block;
257
+ padding: 5px 12px;
258
+ border-radius: 15px;
259
+ font-size: 0.85em;
260
+ font-weight: 600;
261
+ margin-left: 10px;
262
+ }
263
+
264
+ .impact-high {
265
+ background: #ff6b6b;
266
+ color: white;
267
+ }
268
+
269
+ .impact-medium {
270
+ background: #ffa502;
271
+ color: white;
272
+ }
273
+
274
+ .impact-low {
275
+ background: #26de81;
276
+ color: white;
277
+ }
278
+
279
+ /* λͺ¨λΈ μΉ΄λ“œ */
280
+ .model-grid {
281
+ display: grid;
282
+ grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
283
+ gap: 25px;
284
+ margin-top: 30px;
285
+ }
286
+
287
+ .model-card {
288
+ background: white;
289
+ padding: 25px;
290
+ border-radius: 12px;
291
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
292
+ transition: all 0.3s;
293
+ border-top: 4px solid #667eea;
294
+ position: relative;
295
+ }
296
+
297
+ .model-card:hover {
298
+ transform: translateY(-5px);
299
+ box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3);
300
+ }
301
+
302
+ .model-rank {
303
+ position: absolute;
304
+ top: -15px;
305
+ right: 20px;
306
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
307
+ color: white;
308
+ width: 50px;
309
+ height: 50px;
310
+ border-radius: 50%;
311
+ display: flex;
312
+ align-items: center;
313
+ justify-content: center;
314
+ font-weight: 700;
315
+ font-size: 1.2em;
316
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
317
+ }
318
+
319
+ .model-name {
320
+ font-weight: 700;
321
+ color: #667eea;
322
+ margin-bottom: 15px;
323
+ font-size: 1.15em;
324
+ word-break: break-word;
325
+ padding-right: 60px;
326
+ }
327
+
328
+ .model-stats {
329
+ display: grid;
330
+ grid-template-columns: repeat(2, 1fr);
331
+ gap: 10px;
332
+ margin: 15px 0;
333
+ padding: 15px;
334
+ background: #f8f9fa;
335
+ border-radius: 8px;
336
+ }
337
+
338
+ .model-stat-item {
339
+ font-size: 0.9em;
340
+ }
341
+
342
+ .model-task {
343
+ background: #e8f0fe;
344
+ color: #667eea;
345
+ padding: 6px 12px;
346
+ border-radius: 20px;
347
+ font-size: 0.85em;
348
+ display: inline-block;
349
+ margin-bottom: 15px;
350
+ font-weight: 600;
351
+ }
352
+
353
+ .model-analysis {
354
+ background: #f0f4ff;
355
+ padding: 15px;
356
+ border-radius: 8px;
357
+ margin-top: 15px;
358
+ color: #34495e;
359
+ line-height: 1.7;
360
+ font-size: 0.95em;
361
+ }
362
+
363
+ /* 슀페이슀 μΉ΄λ“œ */
364
+ .space-card {
365
+ background: white;
366
+ padding: 25px;
367
+ border-radius: 12px;
368
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
369
+ margin-bottom: 20px;
370
+ border-left: 5px solid #ff6b6b;
371
+ transition: all 0.3s;
372
+ }
373
+
374
+ .space-card:hover {
375
+ transform: translateX(10px);
376
+ box-shadow: 0 10px 25px rgba(255, 107, 107, 0.3);
377
+ }
378
+
379
+ .space-header {
380
+ display: flex;
381
+ justify-content: space-between;
382
+ align-items: flex-start;
383
+ margin-bottom: 15px;
384
+ }
385
+
386
+ .space-name {
387
+ font-weight: 700;
388
+ color: #ff6b6b;
389
+ font-size: 1.3em;
390
+ }
391
+
392
+ .space-badge {
393
+ background: #ff6b6b;
394
+ color: white;
395
+ padding: 5px 12px;
396
+ border-radius: 15px;
397
+ font-size: 0.8em;
398
+ font-weight: 600;
399
+ }
400
+
401
+ .space-description {
402
+ color: #555;
403
+ margin-bottom: 15px;
404
+ line-height: 1.6;
405
+ }
406
+
407
+ .space-analysis {
408
+ background: #fff5f5;
409
+ padding: 15px;
410
+ border-radius: 8px;
411
+ margin-top: 15px;
412
+ }
413
+
414
+ .space-tech {
415
+ display: flex;
416
+ flex-wrap: wrap;
417
+ gap: 8px;
418
+ margin-top: 15px;
419
+ }
420
+
421
+ .tech-tag {
422
+ background: #ffe5e5;
423
+ color: #ff6b6b;
424
+ padding: 5px 10px;
425
+ border-radius: 12px;
426
+ font-size: 0.8em;
427
+ font-weight: 600;
428
+ }
429
+
430
+ /* λ²„νŠΌ */
431
+ .button-group {
432
+ text-align: center;
433
+ margin: 40px 0;
434
+ display: flex;
435
+ justify-content: center;
436
+ gap: 15px;
437
+ flex-wrap: wrap;
438
+ }
439
+
440
+ .refresh-btn {
441
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
442
+ color: white;
443
+ border: none;
444
+ padding: 18px 50px;
445
+ font-size: 1.2em;
446
+ font-weight: 700;
447
+ border-radius: 50px;
448
+ cursor: pointer;
449
+ box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
450
+ transition: all 0.3s;
451
+ }
452
+
453
+ .refresh-btn:hover {
454
+ transform: scale(1.08);
455
+ box-shadow: 0 12px 30px rgba(102, 126, 234, 0.6);
456
+ }
457
+
458
+ .news-link {
459
+ display: inline-block;
460
+ background: #667eea;
461
+ color: white;
462
+ padding: 10px 20px;
463
+ border-radius: 8px;
464
+ text-decoration: none;
465
+ font-size: 0.95em;
466
+ font-weight: 600;
467
+ transition: all 0.3s;
468
+ margin-top: 15px;
469
+ }
470
+
471
+ .news-link:hover {
472
+ background: #764ba2;
473
+ transform: scale(1.05);
474
+ }
475
+
476
+ .loading {
477
+ text-align: center;
478
+ padding: 60px;
479
+ font-size: 1.8em;
480
+ color: #667eea;
481
+ font-weight: 600;
482
+ }
483
+
484
+ .timestamp {
485
+ text-align: center;
486
+ color: #999;
487
+ margin-top: 40px;
488
+ font-size: 1em;
489
+ padding: 20px;
490
+ background: #f8f9fa;
491
+ border-radius: 10px;
492
+ }
493
+
494
+ .footer {
495
+ text-align: center;
496
+ margin-top: 50px;
497
+ padding-top: 30px;
498
+ border-top: 2px solid #e0e0e0;
499
+ color: #666;
500
+ }
501
+
502
+ @keyframes fadeIn {
503
+ from {
504
+ opacity: 0;
505
+ transform: translateY(20px);
506
+ }
507
+ to {
508
+ opacity: 1;
509
+ transform: translateY(0);
510
+ }
511
+ }
512
+
513
+ @media (max-width: 768px) {
514
+ .container {
515
+ padding: 20px;
516
+ }
517
+
518
+ h1 {
519
+ font-size: 2em;
520
+ }
521
+
522
+ .tabs {
523
+ flex-direction: column;
524
+ }
525
+
526
+ .tab {
527
+ width: 100%;
528
+ }
529
+
530
+ .model-grid {
531
+ grid-template-columns: 1fr;
532
+ }
533
+
534
+ .button-group {
535
+ flex-direction: column;
536
+ }
537
+
538
+ .refresh-btn {
539
+ width: 100%;
540
+ }
541
+ }
542
+ </style>
543
+ </head>
544
+ <body>
545
+ <div class="container">
546
+ <h1>πŸ€– AI λ‰΄μŠ€ & ν—ˆκΉ…νŽ˜μ΄μŠ€ LLM 뢄석</h1>
547
+ <p class="subtitle">μ΄ˆλ“±ν•™μƒλ„ μ΄ν•΄ν•˜λŠ” AI νŠΈλ Œλ“œ 뢄석 μ‹œμŠ€ν…œ πŸŽ“</p>
548
+
549
+ <!-- 톡계 μΉ΄λ“œ -->
550
+ <div class="stats">
551
+ <div class="stat-card">
552
+ <div class="stat-number">{{ stats.total_news }}</div>
553
+ <div class="stat-label">πŸ“° λΆ„μ„λœ λ‰΄μŠ€</div>
554
+ </div>
555
+ <div class="stat-card">
556
+ <div class="stat-number">{{ stats.hf_models }}</div>
557
+ <div class="stat-label">πŸ€— νŠΈλ Œλ”© λͺ¨λΈ</div>
558
+ </div>
559
+ <div class="stat-card">
560
+ <div class="stat-number">{{ stats.hf_spaces }}</div>
561
+ <div class="stat-label">πŸš€ 인기 슀페이슀</div>
562
+ </div>
563
+ <div class="stat-card">
564
+ <div class="stat-number">{{ stats.llm_analyses }}</div>
565
+ <div class="stat-label">🧠 LLM 뢄석</div>
566
+ </div>
567
+ </div>
568
+
569
+ <!-- νƒ­ 메뉴 -->
570
+ <div class="tabs">
571
+ <button class="tab active" onclick="switchTab('news')">πŸ“° AI λ‰΄μŠ€ 뢄석</button>
572
+ <button class="tab" onclick="switchTab('models')">πŸ€— νŠΈλ Œλ”© λͺ¨λΈ</button>
573
+ <button class="tab" onclick="switchTab('spaces')">πŸš€ 인기 슀페이슀</button>
574
+ </div>
575
+
576
+ <!-- λ‰΄μŠ€ νƒ­ -->
577
+ <div id="news-content" class="tab-content active">
578
+ {% for article in analyzed_news %}
579
+ <div class="news-card">
580
+ <div class="news-header">
581
+ <div class="news-title">{{ loop.index }}. {{ article.title }}</div>
582
+ <div class="news-meta">
583
+ <span>πŸ“… {{ article.date }}</span>
584
+ <span>πŸ“° {{ article.source }}</span>
585
+ </div>
586
+ </div>
587
+
588
+ <div class="analysis-section">
589
+ <div class="analysis-item">
590
+ <span class="analysis-label">🎯 μ‰¬μš΄ μš”μ•½</span>
591
+ <div class="analysis-content">{{ article.analysis.summary }}</div>
592
+ </div>
593
+
594
+ <div class="analysis-item">
595
+ <span class="analysis-label">πŸ’‘ μ™œ μ€‘μš”ν• κΉŒ?</span>
596
+ <div class="analysis-content">{{ article.analysis.significance }}</div>
597
+ </div>
598
+
599
+ <div class="analysis-item">
600
+ <span class="analysis-label">πŸ“Š 영ν–₯도</span>
601
+ <span class="impact-level impact-{{ article.analysis.impact_level }}">
602
+ {{ article.analysis.impact_text }}
603
+ </span>
604
+ <div class="analysis-content" style="margin-top: 10px;">
605
+ {{ article.analysis.impact_description }}
606
+ </div>
607
+ </div>
608
+
609
+ <div class="analysis-item">
610
+ <span class="analysis-label">βœ… μš°λ¦¬κ°€ ν•  수 μžˆλŠ” 것</span>
611
+ <div class="analysis-content">{{ article.analysis.action }}</div>
612
+ </div>
613
+ </div>
614
+
615
+ <a href="{{ article.url }}" target="_blank" class="news-link">
616
+ πŸ”— 전체 기사 읽어보기
617
+ </a>
618
+ </div>
619
+ {% endfor %}
620
+ </div>
621
+
622
+ <!-- λͺ¨λΈ νƒ­ -->
623
+ <div id="models-content" class="tab-content">
624
+ <div class="model-grid">
625
+ {% for model in analyzed_models %}
626
+ <div class="model-card">
627
+ <div class="model-rank">{{ model.rank }}</div>
628
+ <div class="model-name">{{ model.name }}</div>
629
+ <div class="model-task">🏷️ {{ model.task }}</div>
630
+
631
+ <div class="model-stats">
632
+ <div class="model-stat-item">
633
+ <strong>πŸ“₯ λ‹€μš΄λ‘œλ“œ</strong><br>
634
+ {{ "{:,}".format(model.downloads) }}
635
+ </div>
636
+ <div class="model-stat-item">
637
+ <strong>❀️ μ’‹μ•„μš”</strong><br>
638
+ {{ "{:,}".format(model.likes) }}
639
+ </div>
640
+ </div>
641
+
642
+ <div class="model-analysis">
643
+ <strong>🧠 AI 뢄석:</strong><br>
644
+ {{ model.analysis }}
645
+ </div>
646
+
647
+ <a href="{{ model.url }}" target="_blank" class="news-link">
648
+ πŸ”— λͺ¨λΈ νŽ˜μ΄μ§€ λ°©λ¬Έ
649
+ </a>
650
+ </div>
651
+ {% endfor %}
652
+ </div>
653
+
654
+ {% if analyzed_models|length == 0 %}
655
+ <div class="loading">
656
+ ⚠️ λͺ¨λΈ 데이터λ₯Ό λΆˆλŸ¬μ˜€λŠ” 쀑...<br>
657
+ <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;">
658
+ πŸ”₯ 데이터 μˆ˜μ§‘ν•˜κΈ°
659
+ </button>
660
+ </div>
661
+ {% endif %}
662
+ </div>
663
+
664
+ <!-- 슀페이슀 νƒ­ -->
665
+ <div id="spaces-content" class="tab-content">
666
+ {% for space in analyzed_spaces %}
667
+ <div class="space-card">
668
+ <div class="space-header">
669
+ <div class="space-name">{{ space.rank }}. {{ space.name }}</div>
670
+ <span class="space-badge">νŠΈλ Œλ”© {{ space.rank }}μœ„</span>
671
+ </div>
672
+
673
+ <div class="space-description">
674
+ <strong>πŸ“ μ„€λͺ…:</strong> {{ space.description }}
675
+ </div>
676
+
677
+ <div class="space-analysis">
678
+ <strong>πŸŽ“ μ΄ˆλ“±ν•™μƒ μ„€λͺ…:</strong><br>
679
+ {{ space.simple_explanation }}
680
+ </div>
681
+
682
+ {% if space.tech_stack %}
683
+ <div class="space-tech">
684
+ <strong style="width: 100%; margin-bottom: 5px;">πŸ› οΈ μ‚¬μš© 기술:</strong>
685
+ {% for tech in space.tech_stack %}
686
+ <span class="tech-tag">{{ tech }}</span>
687
+ {% endfor %}
688
+ </div>
689
+ {% endif %}
690
+
691
+ <a href="{{ space.url }}" target="_blank" class="news-link">
692
+ πŸ”— 슀페이슀 μ²΄ν—˜ν•˜κΈ°
693
+ </a>
694
+ </div>
695
+ {% endfor %}
696
+
697
+ {% if analyzed_spaces|length == 0 %}
698
+ <div class="loading">
699
+ ⚠️ 슀페이슀 데이터λ₯Ό λΆˆλŸ¬μ˜€λŠ” 쀑...<br>
700
+ <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;">
701
+ πŸ”₯ 데이터 μˆ˜μ§‘ν•˜κΈ°
702
+ </button>
703
+ </div>
704
+ {% endif %}
705
+ </div>
706
+
707
+ <!-- λ²„νŠΌ κ·Έλ£Ή -->
708
+ <div class="button-group">
709
+ <button class="refresh-btn" onclick="location.reload()">
710
+ πŸ”„ νŽ˜μ΄μ§€ μƒˆλ‘œκ³ μΉ¨
711
+ </button>
712
+ <button class="refresh-btn" onclick="location.href='/?refresh=true'" style="background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);">
713
+ πŸ”₯ 데이터 κ°•μ œ κ°±μ‹ 
714
+ </button>
715
+ </div>
716
+
717
+ <!-- νƒ€μž„μŠ€νƒ¬ν”„ -->
718
+ <div class="timestamp">
719
+ ⏰ λ§ˆμ§€λ§‰ μ—…λ°μ΄νŠΈ: {{ timestamp }}
720
+ </div>
721
+
722
+ <!-- ν‘Έν„° -->
723
+ <div class="footer">
724
+ <p>πŸ€– AI λ‰΄μŠ€ LLM 뢄석 μ‹œμŠ€ν…œ v3.2</p>
725
+ <p style="margin-top: 10px; font-size: 0.9em;">
726
+ πŸ’Ύ SQLite DB 영ꡬ μ €μž₯ | 🌐 AI Times μ‹€μ‹œκ°„ 크둀링 | πŸ€— Hugging Face Trending API | 🧠 Powered by Fireworks AI (Qwen3-235B)
727
+ </p>
728
+ <p style="margin-top: 10px; font-size: 0.85em; color: #999;">
729
+ 데이터 좜처: AI Times (μ‹€μ‹œκ°„ 크둀링), Hugging Face | μ‹€μ‹œκ°„ 뢄석: Fireworks AI
730
+ </p>
731
+ </div>
732
+ </div>
733
+
734
+ <script>
735
+ function switchTab(tabName) {
736
+ // λͺ¨λ“  νƒ­ λΉ„ν™œμ„±ν™”
737
+ document.querySelectorAll('.tab').forEach(tab => {
738
+ tab.classList.remove('active');
739
+ });
740
+ document.querySelectorAll('.tab-content').forEach(content => {
741
+ content.classList.remove('active');
742
+ });
743
+
744
+ // μ„ νƒλœ νƒ­ ν™œμ„±ν™”
745
+ event.target.classList.add('active');
746
+ document.getElementById(tabName + '-content').classList.add('active');
747
+ }
748
+
749
+ console.log('βœ… AI λ‰΄μŠ€ LLM 뢄석 μ‹œμŠ€ν…œ λ‘œλ“œ μ™„λ£Œ');
750
+ </script>
751
+ </body>
752
+ </html>
753
+ """
754
+
755
+
756
+ # ============================================
757
+ # λ°μ΄ν„°λ² μ΄μŠ€ μ΄ˆκΈ°ν™”
758
+ # ============================================
759
+
760
+ def init_database():
761
+ """SQLite λ°μ΄ν„°λ² μ΄μŠ€ μ΄ˆκΈ°ν™”"""
762
+ conn = sqlite3.connect(DB_PATH)
763
+ cursor = conn.cursor()
764
+
765
+ # λ‰΄μŠ€ ν…Œμ΄λΈ”
766
+ cursor.execute('''
767
+ CREATE TABLE IF NOT EXISTS news (
768
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
769
+ title TEXT NOT NULL,
770
+ url TEXT NOT NULL UNIQUE,
771
+ date TEXT,
772
+ source TEXT,
773
+ category TEXT,
774
+ summary TEXT,
775
+ significance TEXT,
776
+ impact_level TEXT,
777
+ impact_text TEXT,
778
+ impact_description TEXT,
779
+ action TEXT,
780
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
781
+ )
782
+ ''')
783
+
784
+ # λͺ¨λΈ ν…Œμ΄λΈ”
785
+ cursor.execute('''
786
+ CREATE TABLE IF NOT EXISTS models (
787
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
788
+ name TEXT NOT NULL UNIQUE,
789
+ downloads INTEGER,
790
+ likes INTEGER,
791
+ task TEXT,
792
+ url TEXT,
793
+ analysis TEXT,
794
+ rank INTEGER,
795
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
796
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
797
+ )
798
+ ''')
799
+
800
+ # ���페이슀 ν…Œμ΄λΈ”
801
+ cursor.execute('''
802
+ CREATE TABLE IF NOT EXISTS spaces (
803
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
804
+ space_id TEXT NOT NULL UNIQUE,
805
+ name TEXT NOT NULL,
806
+ author TEXT,
807
+ title TEXT,
808
+ likes INTEGER,
809
+ url TEXT,
810
+ sdk TEXT,
811
+ simple_explanation TEXT,
812
+ tech_stack TEXT,
813
+ rank INTEGER,
814
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
815
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
816
+ )
817
+ ''')
818
+
819
+ conn.commit()
820
+ conn.close()
821
+ print("βœ… λ°μ΄ν„°λ² μ΄μŠ€ μ΄ˆκΈ°ν™” μ™„λ£Œ")
822
+
823
+
824
+ def save_news_to_db(news_list: List[Dict]):
825
+ """λ‰΄μŠ€ 데이터λ₯Ό DB에 μ €μž₯"""
826
+ conn = sqlite3.connect(DB_PATH)
827
+ cursor = conn.cursor()
828
+
829
+ saved_count = 0
830
+ for news in news_list:
831
+ try:
832
+ cursor.execute('''
833
+ INSERT OR REPLACE INTO news
834
+ (title, url, date, source, category, summary, significance,
835
+ impact_level, impact_text, impact_description, action)
836
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
837
+ ''', (
838
+ news['title'],
839
+ news['url'],
840
+ news.get('date', ''),
841
+ news.get('source', ''),
842
+ news.get('category', ''),
843
+ news['analysis']['summary'],
844
+ news['analysis']['significance'],
845
+ news['analysis']['impact_level'],
846
+ news['analysis']['impact_text'],
847
+ news['analysis']['impact_description'],
848
+ news['analysis']['action']
849
+ ))
850
+ saved_count += 1
851
+ except sqlite3.IntegrityError:
852
+ pass # 이미 μ‘΄μž¬ν•˜λŠ” λ‰΄μŠ€
853
+
854
+ conn.commit()
855
+ conn.close()
856
+ print(f"βœ… {saved_count}개 λ‰΄μŠ€ DB μ €μž₯ μ™„λ£Œ")
857
+
858
+
859
+ def save_models_to_db(models_list: List[Dict]):
860
+ """λͺ¨λΈ 데이터λ₯Ό DB에 μ €μž₯"""
861
+ conn = sqlite3.connect(DB_PATH)
862
+ cursor = conn.cursor()
863
+
864
+ saved_count = 0
865
+ for model in models_list:
866
+ try:
867
+ cursor.execute('''
868
+ INSERT OR REPLACE INTO models
869
+ (name, downloads, likes, task, url, analysis, rank, updated_at)
870
+ VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
871
+ ''', (
872
+ model['name'],
873
+ model['downloads'],
874
+ model['likes'],
875
+ model['task'],
876
+ model['url'],
877
+ model['analysis'],
878
+ model['rank']
879
+ ))
880
+ saved_count += 1
881
+ except Exception as e:
882
+ print(f"⚠️ λͺ¨λΈ μ €μž₯ 였λ₯˜: {e}")
883
+
884
+ conn.commit()
885
+ conn.close()
886
+ print(f"βœ… {saved_count}개 λͺ¨λΈ DB μ €μž₯ μ™„λ£Œ")
887
+
888
+
889
+ def save_spaces_to_db(spaces_list: List[Dict]):
890
+ """슀페이슀 데이터λ₯Ό DB에 μ €μž₯"""
891
+ conn = sqlite3.connect(DB_PATH)
892
+ cursor = conn.cursor()
893
+
894
+ saved_count = 0
895
+ for space in spaces_list:
896
+ try:
897
+ cursor.execute('''
898
+ INSERT OR REPLACE INTO spaces
899
+ (space_id, name, author, title, likes, url, sdk,
900
+ simple_explanation, tech_stack, rank, updated_at)
901
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
902
+ ''', (
903
+ space['space_id'],
904
+ space['name'],
905
+ space.get('author', ''),
906
+ space.get('title', ''),
907
+ space.get('likes', 0),
908
+ space['url'],
909
+ space.get('sdk', ''),
910
+ space['simple_explanation'],
911
+ json.dumps(space.get('tech_stack', [])),
912
+ space['rank']
913
+ ))
914
+ saved_count += 1
915
+ except Exception as e:
916
+ print(f"⚠️ 슀페이슀 μ €μž₯ 였λ₯˜: {e}")
917
+
918
+ conn.commit()
919
+ conn.close()
920
+ print(f"βœ… {saved_count}개 슀페이슀 DB μ €μž₯ μ™„λ£Œ")
921
+
922
+
923
+ def load_news_from_db() -> List[Dict]:
924
+ """DBμ—μ„œ λ‰΄μŠ€ λ‘œλ“œ"""
925
+ conn = sqlite3.connect(DB_PATH)
926
+ cursor = conn.cursor()
927
+
928
+ cursor.execute('''
929
+ SELECT title, url, date, source, category, summary, significance,
930
+ impact_level, impact_text, impact_description, action
931
+ FROM news ORDER BY created_at DESC LIMIT 50
932
+ ''')
933
+
934
+ news_list = []
935
+ for row in cursor.fetchall():
936
+ news_list.append({
937
+ 'title': row[0],
938
+ 'url': row[1],
939
+ 'date': row[2],
940
+ 'source': row[3],
941
+ 'category': row[4],
942
+ 'analysis': {
943
+ 'summary': row[5],
944
+ 'significance': row[6],
945
+ 'impact_level': row[7],
946
+ 'impact_text': row[8],
947
+ 'impact_description': row[9],
948
+ 'action': row[10]
949
+ }
950
+ })
951
+
952
+ conn.close()
953
+ return news_list
954
+
955
+
956
+ def load_models_from_db() -> List[Dict]:
957
+ """DBμ—μ„œ λͺ¨λΈ λ‘œλ“œ"""
958
+ conn = sqlite3.connect(DB_PATH)
959
+ cursor = conn.cursor()
960
+
961
+ cursor.execute('''
962
+ SELECT name, downloads, likes, task, url, analysis, rank
963
+ FROM models ORDER BY rank ASC LIMIT 30
964
+ ''')
965
+
966
+ models_list = []
967
+ for row in cursor.fetchall():
968
+ models_list.append({
969
+ 'name': row[0],
970
+ 'downloads': row[1],
971
+ 'likes': row[2],
972
+ 'task': row[3],
973
+ 'url': row[4],
974
+ 'analysis': row[5],
975
+ 'rank': row[6]
976
+ })
977
+
978
+ conn.close()
979
+ return models_list
980
+
981
+
982
+ def load_spaces_from_db() -> List[Dict]:
983
+ """DBμ—μ„œ 슀페이슀 λ‘œλ“œ"""
984
+ conn = sqlite3.connect(DB_PATH)
985
+ cursor = conn.cursor()
986
+
987
+ cursor.execute('''
988
+ SELECT space_id, name, author, title, likes, url, sdk,
989
+ simple_explanation, tech_stack, rank
990
+ FROM spaces ORDER BY rank ASC LIMIT 30
991
+ ''')
992
+
993
+ spaces_list = []
994
+ for row in cursor.fetchall():
995
+ spaces_list.append({
996
+ 'space_id': row[0],
997
+ 'name': row[1],
998
+ 'author': row[2],
999
+ 'title': row[3],
1000
+ 'likes': row[4],
1001
+ 'url': row[5],
1002
+ 'sdk': row[6],
1003
+ 'simple_explanation': row[7],
1004
+ 'tech_stack': json.loads(row[8]) if row[8] else [],
1005
+ 'rank': row[9],
1006
+ 'description': row[3] # title을 description으둜 μ‚¬μš©
1007
+ })
1008
+
1009
+ conn.close()
1010
+ return spaces_list
1011
+
1012
+
1013
+ # ============================================
1014
+ # LLM 뢄석기 클래슀
1015
+ # ============================================
1016
+
1017
+ class LLMAnalyzer:
1018
+ """Fireworks AI (Qwen3) 기반 LLM 뢄석기"""
1019
+
1020
+ def __init__(self):
1021
+ self.api_key = os.environ.get('FIREWORKS_API_KEY', '')
1022
+ self.api_url = "https://api.fireworks.ai/inference/v1/chat/completions"
1023
+ self.api_available = bool(self.api_key)
1024
+
1025
+ if not self.api_available:
1026
+ print("⚠️ FIREWORKS_API_KEY ν™˜κ²½λ³€μˆ˜κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. ν…œν”Œλ¦Ώ λͺ¨λ“œλ‘œ λ™μž‘ν•©λ‹ˆλ‹€.")
1027
+
1028
+ def call_llm(self, messages: List[Dict], max_tokens: int = 2000) -> str:
1029
+ """Fireworks AI API 호좜"""
1030
+ if not self.api_available:
1031
+ return None
1032
+
1033
+ try:
1034
+ payload = {
1035
+ "model": "accounts/fireworks/models/qwen3-235b-a22b-instruct-2507",
1036
+ "max_tokens": max_tokens,
1037
+ "top_p": 1,
1038
+ "top_k": 40,
1039
+ "presence_penalty": 0,
1040
+ "frequency_penalty": 0,
1041
+ "temperature": 0.6,
1042
+ "messages": messages
1043
+ }
1044
+
1045
+ headers = {
1046
+ "Accept": "application/json",
1047
+ "Content-Type": "application/json",
1048
+ "Authorization": f"Bearer {self.api_key}"
1049
+ }
1050
+
1051
+ response = requests.post(self.api_url, headers=headers, json=payload, timeout=30)
1052
+ response.raise_for_status()
1053
+
1054
+ result = response.json()
1055
+ return result['choices'][0]['message']['content']
1056
+
1057
+ except Exception as e:
1058
+ print(f" ⚠️ LLM API 호좜 였λ₯˜: {e}")
1059
+ return None
1060
+
1061
+ def fetch_model_card(self, model_id: str) -> str:
1062
+ """ν—ˆκΉ…νŽ˜μ΄μŠ€ λͺ¨λΈ μΉ΄λ“œ(README.md) κ°€μ Έμ˜€κΈ°"""
1063
+ try:
1064
+ url = f"https://huggingface.co/{model_id}/raw/main/README.md"
1065
+ response = requests.get(url, timeout=10)
1066
+
1067
+ if response.status_code == 200:
1068
+ content = response.text
1069
+ # λ„ˆλ¬΄ κΈ΄ 경우 μ•žλΆ€λΆ„λ§Œ (μ•½ 3000자)
1070
+ if len(content) > 3000:
1071
+ content = content[:3000] + "\n...(ν›„λž΅)"
1072
+ return content
1073
+ else:
1074
+ return None
1075
+ except Exception as e:
1076
+ print(f" ⚠️ λͺ¨λΈ μΉ΄λ“œ κ°€μ Έμ˜€κΈ° 였λ₯˜: {e}")
1077
+ return None
1078
+
1079
+ def fetch_space_code(self, space_id: str) -> str:
1080
+ """ν—ˆκΉ…νŽ˜μ΄μŠ€ 슀페이슀 app.py κ°€μ Έμ˜€κΈ°"""
1081
+ try:
1082
+ url = f"https://huggingface.co/spaces/{space_id}/raw/main/app.py"
1083
+ response = requests.get(url, timeout=10)
1084
+
1085
+ if response.status_code == 200:
1086
+ content = response.text
1087
+ # λ„ˆλ¬΄ κΈ΄ 경우 μ•žλΆ€λΆ„λ§Œ (μ•½ 2000자)
1088
+ if len(content) > 2000:
1089
+ content = content[:2000] + "\n...(ν›„λž΅)"
1090
+ return content
1091
+ else:
1092
+ return None
1093
+ except Exception as e:
1094
+ print(f" ⚠️ 슀페이슀 μ½”λ“œ κ°€μ Έμ˜€κΈ° 였λ₯˜: {e}")
1095
+ return None
1096
+
1097
+ def analyze_news_simple(self, title: str, content: str = "") -> Dict:
1098
+ """λ‰΄μŠ€ 기사λ₯Ό μ΄ˆλ“±ν•™μƒ μˆ˜μ€€μœΌλ‘œ 뢄석"""
1099
+
1100
+ analysis_templates = {
1101
+ "μ±—GPT": {
1102
+ "summary": "λ§ˆμ΄ν¬λ‘œμ†Œν”„νŠΈ(MS)λΌλŠ” 큰 νšŒμ‚¬κ°€ μ±—GPTλΌλŠ” AIλ₯Ό λ„ˆλ¬΄ λ§Žμ€ μ‚¬λžŒλ“€μ΄ μ‚¬μš©ν•΄μ„œ, 컴퓨터λ₯Ό λ³΄κ΄€ν•˜λŠ” 큰 건물(데이터센터)이 λΆ€μ‘±ν•˜λ‹€κ³  λ§ν–ˆμ–΄μš”.",
1103
+ "significance": "μ±—GPTκ°€ 정말 인기가 λ§Žλ‹€λŠ” λœ»μ΄μ—μš”. 마치 λ„ˆλ¬΄ λ§Žμ€ μΉœκ΅¬λ“€μ΄ ν•œ κ²Œμž„κΈ°λ₯Ό μ“°λ €κ³  ν•˜λŠ” 것과 λΉ„μŠ·ν•΄μš”.",
1104
+ "impact_level": "high",
1105
+ "impact_text": "λ†’μŒ",
1106
+ "impact_description": "AI 기술이 λΉ λ₯΄κ²Œ λ°œμ „ν•˜κ³  있고, λ§Žμ€ μ‚¬λžŒλ“€μ΄ μ‚¬μš©ν•˜κ³  μžˆλ‹€λŠ” μ€‘μš”ν•œ μ‹ ν˜Έμ˜ˆμš”.",
1107
+ "action": "μ±—GPT 같은 AI 도ꡬλ₯Ό λ°°μ›Œλ³΄μ„Έμš”. μˆ™μ œλ₯Ό 도와달라고 ν•˜κ±°λ‚˜, λͺ¨λ₯΄λŠ” 것을 λ¬Όμ–΄λ³Ό 수 μžˆμ–΄μš”!"
1108
+ },
1109
+ "GPU": {
1110
+ "summary": "미ꡭ이 μ•„λžμ—λ―Έλ¦¬νŠΈ(UAE)λΌλŠ” λ‚˜λΌμ— GPUλΌλŠ” νŠΉλ³„ν•œ 컴퓨터 λΆ€ν’ˆμ„ νŒ” 수 있게 ν—ˆλ½ν–ˆμ–΄μš”. GPUλŠ” AIλ₯Ό λ§Œλ“œλŠ” 데 κΌ­ ν•„μš”ν•œ λΆ€ν’ˆμ΄μ—μš”.",
1111
+ "significance": "GPUλŠ” AI의 λ‘λ‡Œ 같은 κ±°μ˜ˆμš”. 이걸 νŒ” 수 있게 되면 더 λ§Žμ€ λ‚˜λΌμ—μ„œ AIλ₯Ό λ§Œλ“€ 수 μžˆμ–΄μš”.",
1112
+ "impact_level": "medium",
1113
+ "impact_text": "쀑간",
1114
+ "impact_description": "AI 기술이 더 λ§Žμ€ λ‚˜λΌλ‘œ 퍼질 수 있게 λ˜μ—ˆμ–΄μš”.",
1115
+ "action": "컴퓨터가 μ–΄λ–»κ²Œ μž‘λ™ν•˜λŠ”μ§€ 관심을 κ°€μ Έλ³΄μ„Έμš”. GPUκ°€ 무엇인지 κ²€μƒ‰ν•΄λ³΄λŠ” 것도 μ’‹μ•„μš”!"
1116
+ },
1117
+ "μ†ŒλΌ": {
1118
+ "summary": "μ˜€ν”ˆAIκ°€ λ§Œλ“  'μ†ŒλΌ'λΌλŠ” AI 앱이 μ—„μ²­ λΉ λ₯΄κ²Œ 인기λ₯Ό μ–»μ—ˆμ–΄μš”. 100만 λͺ…이 λ‹€μš΄λ‘œλ“œν•˜λŠ” 데 μ±—GPT보닀 더 λΉ¨λžλŒ€μš”!",
1119
+ "significance": "μ‚¬λžŒλ“€μ΄ λΉ„λ””μ˜€λ₯Ό λ§Œλ“œλŠ” AI에 정말 관심이 λ§Žλ‹€λŠ” λœ»μ΄μ—μš”.",
1120
+ "impact_level": "high",
1121
+ "impact_text": "λ†’μŒ",
1122
+ "impact_description": "μ•žμœΌλ‘œ λˆ„κ΅¬λ‚˜ μ‰½κ²Œ λ©‹μ§„ λΉ„λ””μ˜€λ₯Ό λ§Œλ“€ 수 있게 될 κ±°μ˜ˆμš”.",
1123
+ "action": "μ†ŒλΌλ₯Ό 써보고, μƒμƒν•œ 것을 λΉ„λ””μ˜€λ‘œ λ§Œλ“€μ–΄λ³΄μ„Έμš”. 창의λ ₯을 λ°œνœ˜ν•  수 μžˆμ–΄μš”!"
1124
+ }
1125
+ }
1126
+
1127
+ # ν‚€μ›Œλ“œ 맀칭으둜 ν…œν”Œλ¦Ώ 선택
1128
+ for keyword, template in analysis_templates.items():
1129
+ if keyword.lower() in title.lower():
1130
+ return template
1131
+
1132
+ # κΈ°λ³Έ 뢄석
1133
+ return {
1134
+ "summary": f"'{title}'λΌλŠ” AI κ΄€λ ¨ λ‰΄μŠ€κ°€ λ‚˜μ™”μ–΄μš”. AI 기술이 계속 λ°œμ „ν•˜κ³  μžˆλ‹€λŠ” μ†Œμ‹μ΄μ—μš”.",
1135
+ "significance": "AIλŠ” 우리 μƒν™œμ„ 더 νŽΈλ¦¬ν•˜κ²Œ λ§Œλ“€μ–΄μ£ΌλŠ” κΈ°μˆ μ΄μ—μš”.",
1136
+ "impact_level": "medium",
1137
+ "impact_text": "쀑간",
1138
+ "impact_description": "AI 기술의 λ°œμ „μ€ 우리 λ―Έλž˜μ— μ€‘μš”ν•œ 영ν–₯을 쀄 κ±°μ˜ˆμš”.",
1139
+ "action": "AI에 λŒ€ν•΄ 더 μ•Œμ•„λ³΄κ³ , AIλ₯Ό ν™œμš©ν•˜λŠ” 방법을 λ°°μ›Œλ³΄μ„Έμš”!"
1140
+ }
1141
+
1142
+ def analyze_model(self, model_name: str, task: str, downloads: int) -> str:
1143
+ """ν—ˆκΉ…νŽ˜μ΄μŠ€ λͺ¨λΈ 뢄석 - λͺ¨λΈ μΉ΄λ“œλ₯Ό LLM으둜 뢄석"""
1144
+
1145
+ # 1. λͺ¨λΈ μΉ΄λ“œ κ°€μ Έμ˜€κΈ°
1146
+ model_card = self.fetch_model_card(model_name)
1147
+
1148
+ # 2. LLM으둜 뢄석
1149
+ if model_card and self.api_available:
1150
+ try:
1151
+ messages = [
1152
+ {
1153
+ "role": "system",
1154
+ "content": "당신은 μ΄ˆλ“±ν•™μƒλ„ 이해할 수 있게 AI λͺ¨λΈμ„ μ‰½κ²Œ μ„€λͺ…ν•˜λŠ” μ „λ¬Έκ°€μž…λ‹ˆλ‹€. ν•œκ΅­μ–΄λ‘œ λ‹΅λ³€ν•˜μ„Έμš”."
1155
+ },
1156
+ {
1157
+ "role": "user",
1158
+ "content": f"""λ‹€μŒμ€ ν—ˆκΉ…νŽ˜μ΄μŠ€ λͺ¨λΈ '{model_name}'의 λͺ¨λΈ μΉ΄λ“œμž…λ‹ˆλ‹€:
1159
+
1160
+ {model_card}
1161
+
1162
+ 이 λͺ¨λΈμ„ μ΄ˆλ“±ν•™μƒμ΄ 이해할 수 μžˆλ„λ‘ 3-4λ¬Έμž₯으둜 μ‰½κ²Œ μ„€λͺ…ν•΄μ£Όμ„Έμš”. λ‹€μŒ λ‚΄μš©μ„ ν¬ν•¨ν•˜μ„Έμš”:
1163
+ 1. 이 λͺ¨λΈμ΄ 무엇을 ν•˜λŠ”μ§€
1164
+ 2. μ–΄λ–€ νŠΉμ§•μ΄ μžˆλŠ”μ§€
1165
+ 3. λˆ„κ°€ μ‚¬μš©ν•˜λ©΄ 쒋은지
1166
+
1167
+ 닡변은 λ°˜λ“œμ‹œ 3-4λ¬Έμž₯의 ν•œκ΅­μ–΄λ‘œλ§Œ μž‘μ„±ν•˜μ„Έμš”."""
1168
+ }
1169
+ ]
1170
+
1171
+ result = self.call_llm(messages, max_tokens=500)
1172
+
1173
+ if result:
1174
+ return result.strip()
1175
+
1176
+ except Exception as e:
1177
+ print(f" ⚠️ λͺ¨λΈ 뢄석 LLM 였λ₯˜: {e}")
1178
+
1179
+ # 3. Fallback: ν…œν”Œλ¦Ώ 기반 μ„€λͺ…
1180
+ task_explanations = {
1181
+ "text-generation": "글을 μžλ™μœΌλ‘œ λ§Œλ“€μ–΄μ£ΌλŠ”",
1182
+ "image-to-text": "사진을 보고 μ„€λͺ…을 μ¨μ£ΌλŠ”",
1183
+ "text-to-image": "글을 읽고 그림을 κ·Έλ €μ£ΌλŠ”",
1184
+ "translation": "λ‹€λ₯Έ μ–Έμ–΄λ‘œ λ²ˆμ—­ν•΄μ£ΌλŠ”",
1185
+ "question-answering": "μ§ˆλ¬Έμ— λ‹΅ν•΄μ£ΌλŠ”",
1186
+ "summarization": "κΈ΄ 글을 짧게 μš”μ•½ν•΄μ£ΌλŠ”",
1187
+ "text-classification": "글을 λΆ„λ₯˜ν•΄μ£ΌλŠ”",
1188
+ "token-classification": "단어λ₯Ό λΆ„μ„ν•΄μ£ΌλŠ”",
1189
+ "fill-mask": "��칸을 μ±„μ›Œμ£ΌλŠ”"
1190
+ }
1191
+
1192
+ task_desc = task_explanations.get(task, "νŠΉλ³„ν•œ κΈ°λŠ₯을 ν•˜λŠ”")
1193
+
1194
+ if downloads > 10000000:
1195
+ popularity = "μ—„μ²­λ‚˜κ²Œ λ§Žμ€"
1196
+ elif downloads > 1000000:
1197
+ popularity = "μ•„μ£Ό λ§Žμ€"
1198
+ elif downloads > 100000:
1199
+ popularity = "λ§Žμ€"
1200
+ else:
1201
+ popularity = "μ–΄λŠ 정도"
1202
+
1203
+ return f"이 λͺ¨λΈμ€ {task_desc} AIμ˜ˆμš”. {popularity} μ‚¬λžŒλ“€μ΄ λ‹€μš΄λ‘œλ“œν•΄μ„œ μ‚¬μš©ν•˜κ³  μžˆμ–΄μš”. {model_name.split('/')[-1]}λΌλŠ” μ΄λ¦„μœΌλ‘œ 유λͺ…ν•΄μš”!"
1204
+
1205
+ def analyze_space(self, space_name: str, space_id: str, description: str) -> Dict:
1206
+ """ν—ˆκΉ…νŽ˜μ΄μŠ€ 슀페이슀 뢄석 - app.pyλ₯Ό LLM으둜 뢄석"""
1207
+
1208
+ # 1. app.py μ½”λ“œ κ°€μ Έμ˜€κΈ°
1209
+ app_code = self.fetch_space_code(space_id)
1210
+
1211
+ # 2. LLM으둜 뢄석
1212
+ if app_code and self.api_available:
1213
+ try:
1214
+ messages = [
1215
+ {
1216
+ "role": "system",
1217
+ "content": "당신은 μ΄ˆλ“±ν•™μƒλ„ 이해할 수 있게 AI μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ μ‰½κ²Œ μ„€λͺ…ν•˜λŠ” μ „λ¬Έκ°€μž…λ‹ˆλ‹€. ν•œκ΅­μ–΄λ‘œ λ‹΅λ³€ν•˜μ„Έμš”."
1218
+ },
1219
+ {
1220
+ "role": "user",
1221
+ "content": f"""λ‹€μŒμ€ ν—ˆκΉ…νŽ˜μ΄μŠ€ 슀페이슀 '{space_name}'의 app.py μ½”λ“œμž…λ‹ˆλ‹€:
1222
+
1223
+ {app_code}
1224
+
1225
+ 이 앱을 μ΄ˆλ“±ν•™μƒμ΄ 이해할 수 μžˆλ„λ‘ 3-4λ¬Έμž₯으둜 μ‰½κ²Œ μ„€λͺ…ν•΄μ£Όμ„Έμš”. λ‹€μŒ λ‚΄μš©μ„ ν¬ν•¨ν•˜μ„Έμš”:
1226
+ 1. 이 앱이 무엇을 ν•˜λŠ”μ§€
1227
+ 2. μ–΄λ–€ κΈ°μˆ μ„ μ‚¬μš©ν•˜λŠ”μ§€
1228
+ 3. μ–΄λ–»κ²Œ ν™œμš©ν•  수 μžˆλŠ”μ§€
1229
+
1230
+ 닡변은 λ°˜λ“œμ‹œ 3-4λ¬Έμž₯의 ν•œκ΅­μ–΄λ‘œλ§Œ μž‘μ„±ν•˜μ„Έμš”."""
1231
+ }
1232
+ ]
1233
+
1234
+ result = self.call_llm(messages, max_tokens=500)
1235
+
1236
+ if result:
1237
+ # 기술 μŠ€νƒ μΆ”μΆœ μ‹œλ„
1238
+ tech_stack = []
1239
+ if 'gradio' in app_code.lower():
1240
+ tech_stack.append('Gradio')
1241
+ if 'streamlit' in app_code.lower():
1242
+ tech_stack.append('Streamlit')
1243
+ if 'transformers' in app_code.lower():
1244
+ tech_stack.append('Transformers')
1245
+ if 'torch' in app_code.lower() or 'pytorch' in app_code.lower():
1246
+ tech_stack.append('PyTorch')
1247
+ if 'tensorflow' in app_code.lower():
1248
+ tech_stack.append('TensorFlow')
1249
+ if 'diffusers' in app_code.lower():
1250
+ tech_stack.append('Diffusers')
1251
+
1252
+ if not tech_stack:
1253
+ tech_stack = ['Python', 'AI']
1254
+
1255
+ return {
1256
+ "simple_explanation": result.strip(),
1257
+ "tech_stack": tech_stack
1258
+ }
1259
+
1260
+ except Exception as e:
1261
+ print(f" ⚠️ 슀페이슀 뢄석 LLM 였λ₯˜: {e}")
1262
+
1263
+ # 3. Fallback: ν…œν”Œλ¦Ώ 기반 μ„€λͺ…
1264
+ return {
1265
+ "simple_explanation": f"{space_name}λŠ” μ›ΉλΈŒλΌμš°μ €μ—μ„œ λ°”λ‘œ AIλ₯Ό μ²΄ν—˜ν•΄λ³Ό 수 μžˆλŠ” κ³³μ΄μ—μš”. μ„€μΉ˜ 없이도 μ‚¬μš©ν•  수 μžˆμ–΄μ„œ νŽΈλ¦¬ν•΄μš”! 마치 온라인 κ²Œμž„μ²˜λŸΌ λ°”λ‘œ μ ‘μ†ν•΄μ„œ AIλ₯Ό μ‚¬μš©ν•  수 μžˆλ‹΅λ‹ˆλ‹€.",
1266
+ "tech_stack": ["Python", "Gradio", "Transformers", "PyTorch"]
1267
+ }
1268
+
1269
+
1270
+ # ============================================
1271
+ # κ³ κΈ‰ 뢄석기 클래슀
1272
+ # ============================================
1273
+
1274
+ class AdvancedAIAnalyzer:
1275
+ """LLM 기반 κ³ κΈ‰ AI λ‰΄μŠ€ 뢄석기"""
1276
+
1277
+ def __init__(self):
1278
+ self.llm_analyzer = LLMAnalyzer()
1279
+ self.huggingface_data = {
1280
+ "models": [],
1281
+ "spaces": []
1282
+ }
1283
+ self.news_data = []
1284
+
1285
+ def fetch_huggingface_models(self, limit: int = 30) -> List[Dict]:
1286
+ """ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© λͺ¨λΈ 30개 μˆ˜μ§‘ (μ‹€μ œ API)"""
1287
+ print(f"πŸ€— ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© λͺ¨λΈ {limit}개 μˆ˜μ§‘ 쀑...")
1288
+
1289
+ models_list = []
1290
+
1291
+ try:
1292
+ # Hugging Face API μ‚¬μš©
1293
+ api = HfApi()
1294
+
1295
+ # trending μˆœμœ„λ‘œ λͺ¨λΈ κ°€μ Έμ˜€κΈ°
1296
+ models = list(api.list_models(
1297
+ sort="trending_score",
1298
+ direction=-1,
1299
+ limit=limit
1300
+ ))
1301
+
1302
+ print(f"πŸ“Š APIμ—μ„œ {len(models)}개 λͺ¨λΈ λ°›μŒ")
1303
+
1304
+ for idx, model in enumerate(models[:limit], 1):
1305
+ try:
1306
+ model_info = {
1307
+ 'name': model.id,
1308
+ 'downloads': getattr(model, 'downloads', 0) or 0,
1309
+ 'likes': getattr(model, 'likes', 0) or 0,
1310
+ 'task': getattr(model, 'pipeline_tag', 'N/A') or 'N/A',
1311
+ 'url': f"https://huggingface.co/{model.id}",
1312
+ 'rank': idx
1313
+ }
1314
+
1315
+ # LLM 뢄석 μΆ”κ°€ (λͺ¨λΈ μΉ΄λ“œ 뢄석)
1316
+ print(f" πŸ” {idx}. {model.id} 뢄석 쀑...")
1317
+ model_info['analysis'] = self.llm_analyzer.analyze_model(
1318
+ model_info['name'],
1319
+ model_info['task'],
1320
+ model_info['downloads']
1321
+ )
1322
+
1323
+ models_list.append(model_info)
1324
+
1325
+ # API rate limit λ°©μ§€λ₯Ό μœ„ν•œ 짧은 λŒ€κΈ°
1326
+ time.sleep(0.5)
1327
+
1328
+ # 진행상황 ν‘œμ‹œ
1329
+ if idx % 5 == 0:
1330
+ print(f" βœ“ {idx}개 λͺ¨λΈ 처리 μ™„λ£Œ...")
1331
+
1332
+ except Exception as e:
1333
+ print(f" ⚠️ λͺ¨λΈ {idx} 처리 였λ₯˜: {e}")
1334
+ continue
1335
+
1336
+ print(f"βœ… {len(models_list)}개 νŠΈλ Œλ”© λͺ¨λΈ μˆ˜μ§‘ μ™„λ£Œ")
1337
+
1338
+ # DB에 μ €μž₯
1339
+ if models_list:
1340
+ save_models_to_db(models_list)
1341
+
1342
+ return models_list
1343
+
1344
+ except Exception as e:
1345
+ print(f"❌ λͺ¨λΈ μˆ˜μ§‘ 였λ₯˜: {e}")
1346
+ print("πŸ’Ύ DBμ—μ„œ 이전 데이터 λ‘œλ“œ μ‹œλ„...")
1347
+ return load_models_from_db()
1348
+
1349
+ def fetch_huggingface_spaces(self, limit: int = 30) -> List[Dict]:
1350
+ """ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© 슀페이슀 30개 μˆ˜μ§‘ (μ‹€μ œ API)"""
1351
+ print(f"πŸš€ ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© 슀페이슀 {limit}개 μˆ˜μ§‘ 쀑...")
1352
+
1353
+ spaces_list = []
1354
+
1355
+ try:
1356
+ # Hugging Face API μ‚¬μš©
1357
+ api = HfApi()
1358
+
1359
+ # trending μˆœμœ„λ‘œ 슀페이슀 κ°€μ Έμ˜€κΈ°
1360
+ spaces = list(api.list_spaces(
1361
+ sort="trending_score",
1362
+ direction=-1,
1363
+ limit=limit
1364
+ ))
1365
+
1366
+ print(f"πŸ“Š APIμ—μ„œ {len(spaces)}개 슀페이슀 λ°›μŒ")
1367
+
1368
+ for idx, space in enumerate(spaces[:limit], 1):
1369
+ try:
1370
+ space_info = {
1371
+ 'space_id': space.id,
1372
+ 'name': space.id.split('/')[-1] if '/' in space.id else space.id,
1373
+ 'author': space.author,
1374
+ 'title': getattr(space, 'title', space.id) or space.id,
1375
+ 'likes': getattr(space, 'likes', 0) or 0,
1376
+ 'url': f"https://huggingface.co/spaces/{space.id}",
1377
+ 'sdk': getattr(space, 'sdk', 'gradio') or 'gradio',
1378
+ 'rank': idx
1379
+ }
1380
+
1381
+ # LLM 뢄석 μΆ”κ°€ (app.py 뢄석)
1382
+ print(f" πŸ” {idx}. {space.id} 뢄석 쀑...")
1383
+ space_analysis = self.llm_analyzer.analyze_space(
1384
+ space_info['name'],
1385
+ space_info['space_id'],
1386
+ space_info['title']
1387
+ )
1388
+
1389
+ space_info['simple_explanation'] = space_analysis['simple_explanation']
1390
+ space_info['tech_stack'] = space_analysis['tech_stack']
1391
+ space_info['description'] = space_info['title']
1392
+
1393
+ spaces_list.append(space_info)
1394
+
1395
+ # API rate limit λ°©μ§€λ₯Ό μœ„ν•œ 짧은 λŒ€κΈ°
1396
+ time.sleep(0.5)
1397
+
1398
+ # 진행상황 ν‘œμ‹œ
1399
+ if idx % 5 == 0:
1400
+ print(f" βœ“ {idx}개 슀페이슀 처리 μ™„λ£Œ...")
1401
+
1402
+ except Exception as e:
1403
+ print(f" ⚠️ 슀페이슀 {idx} 처리 였λ₯˜: {e}")
1404
+ continue
1405
+
1406
+ print(f"βœ… {len(spaces_list)}개 νŠΈλ Œλ”© 슀페이슀 μˆ˜μ§‘ μ™„λ£Œ")
1407
+
1408
+ # DB에 μ €μž₯
1409
+ if spaces_list:
1410
+ save_spaces_to_db(spaces_list)
1411
+
1412
+ return spaces_list
1413
+
1414
+ except Exception as e:
1415
+ print(f"❌ 슀페이슀 μˆ˜μ§‘ 였λ₯˜: {e}")
1416
+ print("πŸ’Ύ DBμ—μ„œ 이전 데이터 λ‘œλ“œ μ‹œλ„...")
1417
+ return load_spaces_from_db()
1418
+
1419
+ def fetch_aitimes_news(self) -> List[Dict]:
1420
+ """AI Timesμ—μ„œ 였늘 λ‚ μ§œ λ‰΄μŠ€ 크둀링"""
1421
+ print("πŸ“° AI Times λ‰΄μŠ€ μˆ˜μ§‘ 쀑...")
1422
+
1423
+ # μˆ˜μ§‘ν•  URL λͺ©λ‘
1424
+ urls = [
1425
+ 'https://www.aitimes.com/news/articleList.html?sc_multi_code=S2&view_type=sm',
1426
+ 'https://www.aitimes.com/news/articleList.html?sc_section_code=S1N24&view_type=sm'
1427
+ ]
1428
+
1429
+ all_news = []
1430
+ today = datetime.now().strftime('%m-%d') # 예: '10-10'
1431
+
1432
+ for url_idx, url in enumerate(urls, 1):
1433
+ try:
1434
+ print(f" πŸ” [{url_idx}/2] μˆ˜μ§‘ 쀑: {url}")
1435
+ response = requests.get(url, timeout=15, headers={
1436
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
1437
+ })
1438
+ response.raise_for_status()
1439
+ response.encoding = 'utf-8'
1440
+
1441
+ soup = BeautifulSoup(response.text, 'html.parser')
1442
+
1443
+ # λ‰΄μŠ€ 기사 μ°ΎκΈ° - λ‹€μ–‘ν•œ νŒ¨ν„΄ μ‹œλ„
1444
+ articles = []
1445
+
1446
+ # νŒ¨ν„΄ 1: article-list-content
1447
+ articles.extend(soup.find_all('div', class_='article-list-content'))
1448
+
1449
+ # νŒ¨ν„΄ 2: list-block
1450
+ articles.extend(soup.find_all('div', class_='list-block'))
1451
+
1452
+ # νŒ¨ν„΄ 3: article-list
1453
+ articles.extend(soup.find_all('li', class_='article-list'))
1454
+
1455
+ # νŒ¨ν„΄ 4: λ‹¨μˆœ article νƒœκ·Έ
1456
+ if not articles:
1457
+ articles.extend(soup.find_all('article'))
1458
+
1459
+ # νŒ¨ν„΄ 5: 리슀트 μ•„μ΄ν…œ
1460
+ if not articles:
1461
+ articles.extend(soup.find_all('li'))
1462
+
1463
+ print(f" β†’ {len(articles)}개 기사 블둝 발견")
1464
+
1465
+ for article in articles:
1466
+ try:
1467
+ # 제λͺ©κ³Ό 링크 μ°ΎκΈ° - μ—¬λŸ¬ 방법 μ‹œλ„
1468
+ title_tag = None
1469
+ link = None
1470
+
1471
+ # a νƒœκ·Έμ—μ„œ μ°ΎκΈ°
1472
+ a_tag = article.find('a')
1473
+ if a_tag:
1474
+ title_tag = a_tag
1475
+ link = a_tag.get('href', '')
1476
+
1477
+ # strong νƒœκ·Έμ—μ„œ 제λͺ© 보강
1478
+ if title_tag:
1479
+ strong_tag = title_tag.find('strong')
1480
+ if strong_tag:
1481
+ title = strong_tag.get_text(strip=True)
1482
+ else:
1483
+ title = title_tag.get_text(strip=True)
1484
+ else:
1485
+ continue
1486
+
1487
+ # 링크 μ •κ·œν™”
1488
+ if link and not link.startswith('http'):
1489
+ if link.startswith('/'):
1490
+ link = 'https://www.aitimes.com' + link
1491
+ else:
1492
+ link = 'https://www.aitimes.com/' + link
1493
+
1494
+ # λ‚ μ§œ μ°ΎκΈ° - μ—¬λŸ¬ νŒ¨ν„΄ μ‹œλ„
1495
+ date_text = ''
1496
+ date_patterns = [
1497
+ {'class': 'list-date'},
1498
+ {'class': 'date'},
1499
+ {'class': 'list-dated'},
1500
+ {'class': 'byline'},
1501
+ {'class': 'info'}
1502
+ ]
1503
+
1504
+ for pattern in date_patterns:
1505
+ date_tag = article.find('span', pattern) or article.find('p', pattern)
1506
+ if date_tag:
1507
+ date_text = date_tag.get_text(strip=True)
1508
+ break
1509
+
1510
+ # λ‚ μ§œκ°€ μ—†μœΌλ©΄ 전체 ν…μŠ€νŠΈμ—μ„œ μ°ΎκΈ°
1511
+ if not date_text:
1512
+ text = article.get_text()
1513
+ import re
1514
+ date_match = re.search(r'\d{2}-\d{2}\s+\d{2}:\d{2}', text)
1515
+ if date_match:
1516
+ date_text = date_match.group()
1517
+
1518
+ # μœ νš¨μ„± 검사
1519
+ if not title or len(title) < 10:
1520
+ continue
1521
+
1522
+ if not link or 'javascript:' in link:
1523
+ continue
1524
+
1525
+ # 였늘 λ‚ μ§œ 필터링 (λ‚ μ§œκ°€ μ—†μœΌλ©΄ 일단 포함)
1526
+ if date_text and today not in date_text:
1527
+ continue
1528
+
1529
+ news_item = {
1530
+ 'title': title,
1531
+ 'url': link,
1532
+ 'date': date_text if date_text else today,
1533
+ 'source': 'AI Times',
1534
+ 'category': 'AI'
1535
+ }
1536
+
1537
+ all_news.append(news_item)
1538
+ print(f" βœ“ μΆ”κ°€: {title[:60]}...")
1539
+
1540
+ except Exception as e:
1541
+ continue
1542
+
1543
+ time.sleep(1) # μ„œλ²„ λΆ€ν•˜ λ°©μ§€
1544
+
1545
+ except Exception as e:
1546
+ print(f" ⚠️ URL μˆ˜μ§‘ 였λ₯˜: {e}")
1547
+ continue
1548
+
1549
+ # 쀑볡 제거 (URL κΈ°μ€€)
1550
+ unique_news = []
1551
+ seen_urls = set()
1552
+ for news in all_news:
1553
+ if news['url'] not in seen_urls:
1554
+ unique_news.append(news)
1555
+ seen_urls.add(news['url'])
1556
+
1557
+ print(f"\nβœ… 총 {len(unique_news)}개 였늘자 λ‰΄μŠ€ μˆ˜μ§‘ μ™„λ£Œ\n")
1558
+
1559
+ # μ΅œμ†Œ 3κ°œλŠ” 보μž₯ (μ—†μœΌλ©΄ μƒ˜ν”Œ μΆ”κ°€)
1560
+ if len(unique_news) < 3:
1561
+ print("⚠️ λ‰΄μŠ€κ°€ λΆ€μ‘±ν•˜μ—¬ 졜근 μƒ˜ν”Œ μΆ”κ°€")
1562
+ sample_news = [
1563
+ {
1564
+ 'title': 'MS "μ±—GPT μˆ˜μš” 폭증으둜 데이터센터 λΆ€μ‘±...2026λ…„κΉŒμ§€ 지속"',
1565
+ 'url': 'https://www.aitimes.com/news/articleView.html?idxno=203055',
1566
+ 'date': '10-10 15:10',
1567
+ 'source': 'AI Times',
1568
+ 'category': 'AI'
1569
+ },
1570
+ {
1571
+ 'title': 'λ―Έκ΅­, UAE에 GPU 판맀 일뢀 승인...μ—”λΉ„λ””μ•„ μ‹œμ΄ 5μ‘°λ‹¬λŸ¬ λˆˆμ•ž',
1572
+ 'url': 'https://www.aitimes.com/news/articleView.html?idxno=203053',
1573
+ 'date': '10-10 14:46',
1574
+ 'source': 'AI Times',
1575
+ 'category': 'AI'
1576
+ },
1577
+ {
1578
+ 'title': 'μ†ŒλΌ, μ±—GPT보닀 빨리 100만 λ‹€μš΄λ‘œλ“œ 돌파',
1579
+ 'url': 'https://www.aitimes.com/news/articleView.html?idxno=203045',
1580
+ 'date': '10-10 12:55',
1581
+ 'source': 'AI Times',
1582
+ 'category': 'AI'
1583
+ }
1584
+ ]
1585
+ for sample in sample_news:
1586
+ if sample['url'] not in seen_urls:
1587
+ unique_news.append(sample)
1588
+
1589
+ return unique_news[:20] # μ΅œλŒ€ 20개
1590
+
1591
+ def analyze_all_news(self) -> List[Dict]:
1592
+ """λͺ¨λ“  λ‰΄μŠ€μ— LLM 뢄석 μΆ”κ°€"""
1593
+ print("πŸ“° λ‰΄μŠ€ LLM 뢄석 μ‹œμž‘...")
1594
+
1595
+ # μ‹€μ œ μ›Ήμ‚¬μ΄νŠΈμ—μ„œ λ‰΄μŠ€ μˆ˜μ§‘
1596
+ news = self.fetch_aitimes_news()
1597
+
1598
+ if not news:
1599
+ print("⚠️ μˆ˜μ§‘λœ λ‰΄μŠ€κ°€ μ—†μŠ΅λ‹ˆλ‹€.")
1600
+ return []
1601
+
1602
+ analyzed_news = []
1603
+
1604
+ for idx, article in enumerate(news, 1):
1605
+ print(f" 🧠 {idx}/{len(news)}: {article['title'][:50]}... 뢄석 쀑")
1606
+
1607
+ analysis = self.llm_analyzer.analyze_news_simple(
1608
+ article['title'],
1609
+ ""
1610
+ )
1611
+
1612
+ article['analysis'] = analysis
1613
+ analyzed_news.append(article)
1614
+
1615
+ print(f"βœ… {len(analyzed_news)}개 λ‰΄μŠ€ 뢄석 μ™„λ£Œ")
1616
+
1617
+ # DB에 μ €μž₯
1618
+ if analyzed_news:
1619
+ save_news_to_db(analyzed_news)
1620
+
1621
+ return analyzed_news
1622
+
1623
+ def get_all_data(self, force_refresh: bool = False) -> Dict:
1624
+ """λͺ¨λ“  데이터 μˆ˜μ§‘ 및 뢄석
1625
+
1626
+ Args:
1627
+ force_refresh: Trueλ©΄ μƒˆλ‘œ μˆ˜μ§‘, Falseλ©΄ DBμ—μ„œ λ‘œλ“œ ν›„ μ—†μœΌλ©΄ μˆ˜μ§‘
1628
+ """
1629
+ print("\n" + "="*60)
1630
+ print("πŸš€ AI λ‰΄μŠ€ & ν—ˆκΉ…νŽ˜μ΄μŠ€ LLM 뢄석 μ‹œμž‘")
1631
+ print("="*60 + "\n")
1632
+
1633
+ if force_refresh:
1634
+ print("πŸ”„ κ°•μ œ μƒˆλ‘œκ³ μΉ¨ λͺ¨λ“œ: λͺ¨λ“  데이터 μƒˆλ‘œ μˆ˜μ§‘")
1635
+ analyzed_news = self.analyze_all_news()
1636
+ analyzed_models = self.fetch_huggingface_models(30)
1637
+ analyzed_spaces = self.fetch_huggingface_spaces(30)
1638
+ else:
1639
+ print("πŸ’Ύ DB μš°μ„  λ‘œλ“œ λͺ¨λ“œ")
1640
+
1641
+ # DBμ—μ„œ λ¨Όμ € λ‘œλ“œ
1642
+ analyzed_news = load_news_from_db()
1643
+ if not analyzed_news:
1644
+ print("πŸ“° DB에 λ‰΄μŠ€ μ—†μŒ β†’ μƒˆλ‘œ μˆ˜μ§‘")
1645
+ analyzed_news = self.analyze_all_news()
1646
+ else:
1647
+ print(f"βœ… DBμ—μ„œ {len(analyzed_news)}개 λ‰΄μŠ€ λ‘œλ“œ")
1648
+
1649
+ analyzed_models = load_models_from_db()
1650
+ if not analyzed_models:
1651
+ print("πŸ€— DB에 λͺ¨λΈ μ—†μŒ β†’ μƒˆλ‘œ μˆ˜μ§‘")
1652
+ analyzed_models = self.fetch_huggingface_models(30)
1653
+ else:
1654
+ print(f"βœ… DBμ—μ„œ {len(analyzed_models)}개 λͺ¨λΈ λ‘œλ“œ")
1655
+
1656
+ analyzed_spaces = load_spaces_from_db()
1657
+ if not analyzed_spaces:
1658
+ print("πŸš€ DB에 슀페이슀 μ—†μŒ β†’ μƒˆλ‘œ μˆ˜μ§‘")
1659
+ analyzed_spaces = self.fetch_huggingface_spaces(30)
1660
+ else:
1661
+ print(f"βœ… DBμ—μ„œ {len(analyzed_spaces)}개 슀페이슀 λ‘œλ“œ")
1662
+
1663
+ # 톡계
1664
+ stats = {
1665
+ 'total_news': len(analyzed_news),
1666
+ 'hf_models': len(analyzed_models),
1667
+ 'hf_spaces': len(analyzed_spaces),
1668
+ 'llm_analyses': len(analyzed_news) + len(analyzed_models) + len(analyzed_spaces)
1669
+ }
1670
+
1671
+ print(f"\nβœ… 전체 뢄석 μ™„λ£Œ: {stats['llm_analyses']}개 ν•­λͺ©")
1672
+ print(f" πŸ“° λ‰΄μŠ€: {stats['total_news']}개")
1673
+ print(f" πŸ€— λͺ¨λΈ: {stats['hf_models']}개")
1674
+ print(f" πŸš€ 슀페이슀: {stats['hf_spaces']}개")
1675
+
1676
+ return {
1677
+ 'analyzed_news': analyzed_news,
1678
+ 'analyzed_models': analyzed_models,
1679
+ 'analyzed_spaces': analyzed_spaces,
1680
+ 'stats': stats,
1681
+ 'timestamp': datetime.now().strftime('%Yλ…„ %mμ›” %d일 %H:%M:%S')
1682
+ }
1683
+
1684
+
1685
+ # ============================================
1686
+ # Flask 라우트
1687
+ # ============================================
1688
+
1689
+ @app.route('/')
1690
+ def index():
1691
+ """메인 νŽ˜μ΄μ§€"""
1692
+ try:
1693
+ # refresh νŒŒλΌλ―Έν„° 확인
1694
+ force_refresh = request.args.get('refresh', 'false').lower() == 'true'
1695
+
1696
+ analyzer = AdvancedAIAnalyzer()
1697
+ data = analyzer.get_all_data(force_refresh=force_refresh)
1698
+ return render_template_string(HTML_TEMPLATE, **data)
1699
+ except Exception as e:
1700
+ import traceback
1701
+ error_detail = traceback.format_exc()
1702
+ return f"""
1703
+ <html>
1704
+ <body style="font-family: Arial; padding: 50px; text-align: center;">
1705
+ <h1 style="color: #e74c3c;">⚠️ 였λ₯˜ λ°œμƒ</h1>
1706
+ <p>{str(e)}</p>
1707
+ <pre style="text-align: left; background: #f5f5f5; padding: 20px; border-radius: 5px;">
1708
+ {error_detail}
1709
+ </pre>
1710
+ <button onclick="location.href='/'" style="padding: 10px 20px; margin: 10px;">
1711
+ πŸ”„ μƒˆλ‘œκ³ μΉ¨
1712
+ </button>
1713
+ <button onclick="location.href='/?refresh=true'" style="padding: 10px 20px; margin: 10px; background: #ff6b6b; color: white; border: none; border-radius: 5px;">
1714
+ πŸ”₯ κ°•μ œ κ°±μ‹ 
1715
+ </button>
1716
+ </body>
1717
+ </html>
1718
+ """, 500
1719
+
1720
+
1721
+ @app.route('/api/data')
1722
+ def api_data():
1723
+ """JSON API"""
1724
+ try:
1725
+ force_refresh = request.args.get('refresh', 'false').lower() == 'true'
1726
+ analyzer = AdvancedAIAnalyzer()
1727
+ data = analyzer.get_all_data(force_refresh=force_refresh)
1728
+ return jsonify({
1729
+ 'success': True,
1730
+ 'data': data
1731
+ })
1732
+ except Exception as e:
1733
+ return jsonify({
1734
+ 'success': False,
1735
+ 'error': str(e)
1736
+ }), 500
1737
+
1738
+
1739
+ @app.route('/api/refresh')
1740
+ def api_refresh():
1741
+ """κ°•μ œ μƒˆλ‘œκ³ μΉ¨ API"""
1742
+ try:
1743
+ analyzer = AdvancedAIAnalyzer()
1744
+ data = analyzer.get_all_data(force_refresh=True)
1745
+ return jsonify({
1746
+ 'success': True,
1747
+ 'message': '데이터가 μ„±κ³΅μ μœΌλ‘œ κ°±μ‹ λ˜μ—ˆμŠ΅λ‹ˆλ‹€',
1748
+ 'stats': data['stats']
1749
+ })
1750
+ except Exception as e:
1751
+ return jsonify({
1752
+ 'success': False,
1753
+ 'error': str(e)
1754
+ }), 500
1755
+
1756
+
1757
+ @app.route('/health')
1758
+ def health():
1759
+ """ν—¬μŠ€ 체크"""
1760
+ try:
1761
+ # DB μ—°κ²° 확인
1762
+ conn = sqlite3.connect(DB_PATH)
1763
+ cursor = conn.cursor()
1764
+ cursor.execute("SELECT COUNT(*) FROM news")
1765
+ news_count = cursor.fetchone()[0]
1766
+ cursor.execute("SELECT COUNT(*) FROM models")
1767
+ models_count = cursor.fetchone()[0]
1768
+ cursor.execute("SELECT COUNT(*) FROM spaces")
1769
+ spaces_count = cursor.fetchone()[0]
1770
+ conn.close()
1771
+
1772
+ return jsonify({
1773
+ "status": "healthy",
1774
+ "service": "AI News LLM Analyzer",
1775
+ "version": "3.2.0",
1776
+ "database": {
1777
+ "connected": True,
1778
+ "news_count": news_count,
1779
+ "models_count": models_count,
1780
+ "spaces_count": spaces_count
1781
+ },
1782
+ "fireworks_api": {
1783
+ "configured": bool(os.environ.get('FIREWORKS_API_KEY'))
1784
+ },
1785
+ "timestamp": datetime.now().isoformat()
1786
+ })
1787
+ except Exception as e:
1788
+ return jsonify({
1789
+ "status": "unhealthy",
1790
+ "error": str(e)
1791
+ }), 500
1792
+
1793
+
1794
+ # ============================================
1795
+ # 메인 μ‹€ν–‰
1796
+ # ============================================
1797
+
1798
+ if __name__ == '__main__':
1799
+ port = int(os.environ.get('PORT', 7860))
1800
+
1801
+ print(f"""
1802
+ ╔════════════════════════════════════════════════════════════╗
1803
+ β•‘ β•‘
1804
+ β•‘ πŸ€– AI λ‰΄μŠ€ & ν—ˆκΉ…νŽ˜μ΄μŠ€ LLM 뢄석 μ›Ήμ•± v3.2 β•‘
1805
+ β•‘ β•‘
1806
+ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
1807
+
1808
+ ✨ μ£Όμš” κΈ°λŠ₯:
1809
+ β€’ πŸ’Ύ SQLite DB 영ꡬ μŠ€ν† λ¦¬μ§€
1810
+ β€’ 🌐 AI Times μ‹€μ‹œκ°„ λ‰΄μŠ€ 크둀링 (2개 μ„Ήμ…˜)
1811
+ β€’ πŸ“° λ‰΄μŠ€ μ΄ˆλ“±ν•™μƒ μˆ˜μ€€ 뢄석
1812
+ β€’ πŸ€— ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© λͺ¨λΈ TOP 30 (λͺ¨λΈ μΉ΄λ“œ 뢄석)
1813
+ β€’ πŸš€ ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© 슀페이슀 TOP 30 (app.py 뢄석)
1814
+ β€’ 🧠 Fireworks AI (Qwen3-235B) μ‹€μ‹œκ°„ LLM 뢄석
1815
+ β€’ 🎨 νƒ­ UI (λ‰΄μŠ€/λͺ¨λΈ/슀페이슀)
1816
+
1817
+ πŸ”‘ API μ„€μ •:
1818
+ FIREWORKS_API_KEY: {"βœ… 섀정됨" if os.environ.get('FIREWORKS_API_KEY') else "❌ λ―Έμ„€μ • (ν…œν”Œλ¦Ώ λͺ¨λ“œ)"}
1819
+
1820
+ πŸš€ μ„œλ²„ 정보:
1821
+ πŸ“ 메인: http://localhost:{port}
1822
+ πŸ”„ κ°•μ œκ°±μ‹ : http://localhost:{port}/?refresh=true
1823
+ πŸ“Š API: http://localhost:{port}/api/data
1824
+ πŸ”₯ μƒˆλ‘œκ³ μΉ¨ API: http://localhost:{port}/api/refresh
1825
+ πŸ’š Health: http://localhost:{port}/health
1826
+
1827
+ πŸ’Ύ λ°μ΄ν„°λ² μ΄μŠ€: {DB_PATH}
1828
+
1829
+ μ΄ˆκΈ°ν™” 쀑...
1830
+ """)
1831
+
1832
+ # λ°μ΄ν„°λ² μ΄μŠ€ μ΄ˆκΈ°ν™”
1833
+ try:
1834
+ init_database()
1835
+ except Exception as e:
1836
+ print(f"❌ DB μ΄ˆκΈ°ν™” 였λ₯˜: {e}")
1837
+ sys.exit(1)
1838
+
1839
+ print("\nβœ… μ„œλ²„ μ€€λΉ„ μ™„λ£Œ!")
1840
+ print("λΈŒλΌμš°μ €μ—μ„œ μœ„ URL을 μ—΄μ–΄μ£Όμ„Έμš”!")
1841
+ print("μ’…λ£Œ: Ctrl+C\n")
1842
+
1843
+ try:
1844
+ app.run(
1845
+ host='0.0.0.0',
1846
+ port=port,
1847
+ debug=False,
1848
+ threaded=True
1849
+ )
1850
+ except KeyboardInterrupt:
1851
+ print("\n\nπŸ‘‹ μ„œλ²„ μ’…λ£Œ!")
1852
+ sys.exit(0)
1853
+ except Exception as e:
1854
+ print(f"\n❌ μ„œλ²„ 였λ₯˜: {e}")
1855
+ sys.exit(1)