ginipick commited on
Commit
2d72c19
·
verified ·
1 Parent(s): 4f742ef

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +1642 -0
app.py ADDED
@@ -0,0 +1,1642 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ AI 뉴스 & 허깅페이스 트렌딩 LLM 분석 웹앱 (Fireworks AI 완전판 v3.1)
4
+ 파일명: app_fireworks.py
5
+
6
+ 주요 기능:
7
+ 1. Fireworks AI API로 실제 LLM 분석 (Qwen3-235B 모델)
8
+ 2. SQLite DB 영구 스토리지
9
+ 3. 실제 Hugging Face Trending API 연동 (모델/스페이스 30위)
10
+ 4. 초등학생 수준 분석 (요약/의미/영향/행동지침)
11
+ 5. 탭 UI (뉴스/모델/스페이스)
12
+
13
+ 실행 방법:
14
+ 1. pip install Flask requests beautifulsoup4 huggingface_hub
15
+ 2. export FIREWORKS_API_KEY="your-api-key-here"
16
+ 3. python app_fireworks.py
17
+ 4. 브라우저에서 http://localhost:7860 접속
18
+ """
19
+
20
+ from flask import Flask, render_template_string, jsonify, request
21
+ import requests
22
+ import json
23
+ from datetime import datetime
24
+ from typing import List, Dict, Optional
25
+ import os
26
+ import sys
27
+ import sqlite3
28
+ import time
29
+ from huggingface_hub import HfApi
30
+
31
+ # Flask 앱 초기화
32
+ app = Flask(__name__)
33
+ app.config['JSON_AS_ASCII'] = False
34
+
35
+ # 데이터베이스 파일 경로
36
+ DB_PATH = 'ai_news_analysis.db'
37
+
38
+ # Fireworks AI API 설정
39
+ FIREWORKS_API_KEY = os.environ.get('FIREWORKS_API_KEY', '')
40
+ FIREWORKS_API_URL = "https://api.fireworks.ai/inference/v1/chat/completions"
41
+
42
+
43
+ # ============================================
44
+ # HTML 템플릿 (탭 UI 포함)
45
+ # ============================================
46
+
47
+ HTML_TEMPLATE = """
48
+ <!DOCTYPE html>
49
+ <html lang="ko">
50
+ <head>
51
+ <meta charset="UTF-8">
52
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
53
+ <title>AI 뉴스 & 허깅페이스 LLM 분석 시스템</title>
54
+ <style>
55
+ * {
56
+ margin: 0;
57
+ padding: 0;
58
+ box-sizing: border-box;
59
+ }
60
+
61
+ body {
62
+ font-family: 'Segoe UI', 'Apple SD Gothic Neo', 'Malgun Gothic', sans-serif;
63
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
64
+ padding: 20px;
65
+ color: #333;
66
+ min-height: 100vh;
67
+ }
68
+
69
+ .container {
70
+ max-width: 1400px;
71
+ margin: 0 auto;
72
+ background: white;
73
+ border-radius: 20px;
74
+ padding: 40px;
75
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
76
+ }
77
+
78
+ h1 {
79
+ text-align: center;
80
+ color: #667eea;
81
+ margin-bottom: 10px;
82
+ font-size: 2.8em;
83
+ font-weight: 800;
84
+ }
85
+
86
+ .subtitle {
87
+ text-align: center;
88
+ color: #666;
89
+ margin-bottom: 40px;
90
+ font-size: 1.2em;
91
+ }
92
+
93
+ /* 탭 스타일 */
94
+ .tabs {
95
+ display: flex;
96
+ gap: 15px;
97
+ margin-bottom: 30px;
98
+ border-bottom: 3px solid #e0e0e0;
99
+ padding-bottom: 0;
100
+ }
101
+
102
+ .tab {
103
+ padding: 15px 30px;
104
+ background: #f5f5f5;
105
+ border: none;
106
+ border-radius: 10px 10px 0 0;
107
+ cursor: pointer;
108
+ font-size: 1.1em;
109
+ font-weight: 600;
110
+ color: #666;
111
+ transition: all 0.3s;
112
+ }
113
+
114
+ .tab.active {
115
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
116
+ color: white;
117
+ transform: translateY(-3px);
118
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
119
+ }
120
+
121
+ .tab:hover {
122
+ background: #e0e0e0;
123
+ }
124
+
125
+ .tab.active:hover {
126
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
127
+ }
128
+
129
+ .tab-content {
130
+ display: none;
131
+ }
132
+
133
+ .tab-content.active {
134
+ display: block;
135
+ animation: fadeIn 0.5s ease-out;
136
+ }
137
+
138
+ /* 통계 카드 */
139
+ .stats {
140
+ display: grid;
141
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
142
+ gap: 25px;
143
+ margin-bottom: 50px;
144
+ }
145
+
146
+ .stat-card {
147
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
148
+ color: white;
149
+ padding: 30px;
150
+ border-radius: 15px;
151
+ text-align: center;
152
+ box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
153
+ transform: translateY(0);
154
+ transition: transform 0.3s, box-shadow 0.3s;
155
+ }
156
+
157
+ .stat-card:hover {
158
+ transform: translateY(-5px);
159
+ box-shadow: 0 12px 30px rgba(102, 126, 234, 0.6);
160
+ }
161
+
162
+ .stat-number {
163
+ font-size: 3.5em;
164
+ font-weight: bold;
165
+ margin-bottom: 10px;
166
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
167
+ }
168
+
169
+ .stat-label {
170
+ font-size: 1.2em;
171
+ opacity: 0.95;
172
+ font-weight: 500;
173
+ }
174
+
175
+ /* 뉴스 카드 (LLM 분석 버전) */
176
+ .news-card {
177
+ background: white;
178
+ border-radius: 15px;
179
+ padding: 30px;
180
+ margin-bottom: 25px;
181
+ box-shadow: 0 5px 20px rgba(0,0,0,0.1);
182
+ border-left: 6px solid #667eea;
183
+ transition: all 0.3s;
184
+ }
185
+
186
+ .news-card:hover {
187
+ transform: translateX(10px);
188
+ box-shadow: 0 10px 30px rgba(0,0,0,0.15);
189
+ }
190
+
191
+ .news-header {
192
+ display: flex;
193
+ justify-content: space-between;
194
+ align-items: flex-start;
195
+ margin-bottom: 20px;
196
+ flex-wrap: wrap;
197
+ gap: 15px;
198
+ }
199
+
200
+ .news-title {
201
+ font-size: 1.4em;
202
+ font-weight: 700;
203
+ color: #2c3e50;
204
+ flex: 1;
205
+ min-width: 300px;
206
+ }
207
+
208
+ .news-meta {
209
+ display: flex;
210
+ gap: 15px;
211
+ color: #7f8c8d;
212
+ font-size: 0.9em;
213
+ }
214
+
215
+ .analysis-section {
216
+ background: #f8f9fa;
217
+ padding: 20px;
218
+ border-radius: 10px;
219
+ margin-top: 15px;
220
+ }
221
+
222
+ .analysis-item {
223
+ margin-bottom: 20px;
224
+ padding-bottom: 20px;
225
+ border-bottom: 1px solid #e0e0e0;
226
+ }
227
+
228
+ .analysis-item:last-child {
229
+ border-bottom: none;
230
+ margin-bottom: 0;
231
+ padding-bottom: 0;
232
+ }
233
+
234
+ .analysis-label {
235
+ display: inline-block;
236
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
237
+ color: white;
238
+ padding: 8px 15px;
239
+ border-radius: 20px;
240
+ font-size: 0.9em;
241
+ font-weight: 600;
242
+ margin-bottom: 10px;
243
+ }
244
+
245
+ .analysis-content {
246
+ color: #34495e;
247
+ line-height: 1.8;
248
+ font-size: 1.05em;
249
+ }
250
+
251
+ .impact-level {
252
+ display: inline-block;
253
+ padding: 5px 12px;
254
+ border-radius: 15px;
255
+ font-size: 0.85em;
256
+ font-weight: 600;
257
+ margin-left: 10px;
258
+ }
259
+
260
+ .impact-high {
261
+ background: #ff6b6b;
262
+ color: white;
263
+ }
264
+
265
+ .impact-medium {
266
+ background: #ffa502;
267
+ color: white;
268
+ }
269
+
270
+ .impact-low {
271
+ background: #26de81;
272
+ color: white;
273
+ }
274
+
275
+ /* 모델 카드 */
276
+ .model-grid {
277
+ display: grid;
278
+ grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
279
+ gap: 25px;
280
+ margin-top: 30px;
281
+ }
282
+
283
+ .model-card {
284
+ background: white;
285
+ padding: 25px;
286
+ border-radius: 12px;
287
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
288
+ transition: all 0.3s;
289
+ border-top: 4px solid #667eea;
290
+ position: relative;
291
+ }
292
+
293
+ .model-card:hover {
294
+ transform: translateY(-5px);
295
+ box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3);
296
+ }
297
+
298
+ .model-rank {
299
+ position: absolute;
300
+ top: -15px;
301
+ right: 20px;
302
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
303
+ color: white;
304
+ width: 50px;
305
+ height: 50px;
306
+ border-radius: 50%;
307
+ display: flex;
308
+ align-items: center;
309
+ justify-content: center;
310
+ font-weight: 700;
311
+ font-size: 1.2em;
312
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
313
+ }
314
+
315
+ .model-name {
316
+ font-weight: 700;
317
+ color: #667eea;
318
+ margin-bottom: 15px;
319
+ font-size: 1.15em;
320
+ word-break: break-word;
321
+ padding-right: 60px;
322
+ }
323
+
324
+ .model-stats {
325
+ display: grid;
326
+ grid-template-columns: repeat(2, 1fr);
327
+ gap: 10px;
328
+ margin: 15px 0;
329
+ padding: 15px;
330
+ background: #f8f9fa;
331
+ border-radius: 8px;
332
+ }
333
+
334
+ .model-stat-item {
335
+ font-size: 0.9em;
336
+ }
337
+
338
+ .model-task {
339
+ background: #e8f0fe;
340
+ color: #667eea;
341
+ padding: 6px 12px;
342
+ border-radius: 20px;
343
+ font-size: 0.85em;
344
+ display: inline-block;
345
+ margin-bottom: 15px;
346
+ font-weight: 600;
347
+ }
348
+
349
+ .model-analysis {
350
+ background: #f0f4ff;
351
+ padding: 15px;
352
+ border-radius: 8px;
353
+ margin-top: 15px;
354
+ color: #34495e;
355
+ line-height: 1.7;
356
+ font-size: 0.95em;
357
+ }
358
+
359
+ /* 스페이스 카드 */
360
+ .space-card {
361
+ background: white;
362
+ padding: 25px;
363
+ border-radius: 12px;
364
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
365
+ margin-bottom: 20px;
366
+ border-left: 5px solid #ff6b6b;
367
+ transition: all 0.3s;
368
+ }
369
+
370
+ .space-card:hover {
371
+ transform: translateX(10px);
372
+ box-shadow: 0 10px 25px rgba(255, 107, 107, 0.3);
373
+ }
374
+
375
+ .space-header {
376
+ display: flex;
377
+ justify-content: space-between;
378
+ align-items: flex-start;
379
+ margin-bottom: 15px;
380
+ }
381
+
382
+ .space-name {
383
+ font-weight: 700;
384
+ color: #ff6b6b;
385
+ font-size: 1.3em;
386
+ }
387
+
388
+ .space-badge {
389
+ background: #ff6b6b;
390
+ color: white;
391
+ padding: 5px 12px;
392
+ border-radius: 15px;
393
+ font-size: 0.8em;
394
+ font-weight: 600;
395
+ }
396
+
397
+ .space-description {
398
+ color: #555;
399
+ margin-bottom: 15px;
400
+ line-height: 1.6;
401
+ }
402
+
403
+ .space-analysis {
404
+ background: #fff5f5;
405
+ padding: 15px;
406
+ border-radius: 8px;
407
+ margin-top: 15px;
408
+ }
409
+
410
+ .space-tech {
411
+ display: flex;
412
+ flex-wrap: wrap;
413
+ gap: 8px;
414
+ margin-top: 15px;
415
+ }
416
+
417
+ .tech-tag {
418
+ background: #ffe5e5;
419
+ color: #ff6b6b;
420
+ padding: 5px 10px;
421
+ border-radius: 12px;
422
+ font-size: 0.8em;
423
+ font-weight: 600;
424
+ }
425
+
426
+ /* 버튼 */
427
+ .button-group {
428
+ text-align: center;
429
+ margin: 40px 0;
430
+ display: flex;
431
+ justify-content: center;
432
+ gap: 15px;
433
+ flex-wrap: wrap;
434
+ }
435
+
436
+ .refresh-btn {
437
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
438
+ color: white;
439
+ border: none;
440
+ padding: 18px 50px;
441
+ font-size: 1.2em;
442
+ font-weight: 700;
443
+ border-radius: 50px;
444
+ cursor: pointer;
445
+ box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
446
+ transition: all 0.3s;
447
+ }
448
+
449
+ .refresh-btn:hover {
450
+ transform: scale(1.08);
451
+ box-shadow: 0 12px 30px rgba(102, 126, 234, 0.6);
452
+ }
453
+
454
+ .news-link {
455
+ display: inline-block;
456
+ background: #667eea;
457
+ color: white;
458
+ padding: 10px 20px;
459
+ border-radius: 8px;
460
+ text-decoration: none;
461
+ font-size: 0.95em;
462
+ font-weight: 600;
463
+ transition: all 0.3s;
464
+ margin-top: 15px;
465
+ }
466
+
467
+ .news-link:hover {
468
+ background: #764ba2;
469
+ transform: scale(1.05);
470
+ }
471
+
472
+ .loading {
473
+ text-align: center;
474
+ padding: 60px;
475
+ font-size: 1.8em;
476
+ color: #667eea;
477
+ font-weight: 600;
478
+ }
479
+
480
+ .timestamp {
481
+ text-align: center;
482
+ color: #999;
483
+ margin-top: 40px;
484
+ font-size: 1em;
485
+ padding: 20px;
486
+ background: #f8f9fa;
487
+ border-radius: 10px;
488
+ }
489
+
490
+ .footer {
491
+ text-align: center;
492
+ margin-top: 50px;
493
+ padding-top: 30px;
494
+ border-top: 2px solid #e0e0e0;
495
+ color: #666;
496
+ }
497
+
498
+ @keyframes fadeIn {
499
+ from {
500
+ opacity: 0;
501
+ transform: translateY(20px);
502
+ }
503
+ to {
504
+ opacity: 1;
505
+ transform: translateY(0);
506
+ }
507
+ }
508
+
509
+ @media (max-width: 768px) {
510
+ .container {
511
+ padding: 20px;
512
+ }
513
+
514
+ h1 {
515
+ font-size: 2em;
516
+ }
517
+
518
+ .tabs {
519
+ flex-direction: column;
520
+ }
521
+
522
+ .tab {
523
+ width: 100%;
524
+ }
525
+
526
+ .model-grid {
527
+ grid-template-columns: 1fr;
528
+ }
529
+
530
+ .button-group {
531
+ flex-direction: column;
532
+ }
533
+
534
+ .refresh-btn {
535
+ width: 100%;
536
+ }
537
+ }
538
+ </style>
539
+ </head>
540
+ <body>
541
+ <div class="container">
542
+ <h1>🤖 AI 뉴스 & 허깅페이스 LLM 분석</h1>
543
+ <p class="subtitle">초등학생도 이해하는 AI 트렌드 분석 시스템 🎓 (Powered by Fireworks AI)</p>
544
+
545
+ <!-- 통계 카드 -->
546
+ <div class="stats">
547
+ <div class="stat-card">
548
+ <div class="stat-number">{{ stats.total_news }}</div>
549
+ <div class="stat-label">📰 분석된 뉴스</div>
550
+ </div>
551
+ <div class="stat-card">
552
+ <div class="stat-number">{{ stats.hf_models }}</div>
553
+ <div class="stat-label">🤗 트렌딩 모��</div>
554
+ </div>
555
+ <div class="stat-card">
556
+ <div class="stat-number">{{ stats.hf_spaces }}</div>
557
+ <div class="stat-label">🚀 인기 스페이스</div>
558
+ </div>
559
+ <div class="stat-card">
560
+ <div class="stat-number">{{ stats.llm_analyses }}</div>
561
+ <div class="stat-label">🧠 LLM 분석</div>
562
+ </div>
563
+ </div>
564
+
565
+ <!-- 탭 메뉴 -->
566
+ <div class="tabs">
567
+ <button class="tab active" onclick="switchTab('news')">📰 AI 뉴스 분석</button>
568
+ <button class="tab" onclick="switchTab('models')">🤗 트렌딩 모델</button>
569
+ <button class="tab" onclick="switchTab('spaces')">🚀 인기 스페이스</button>
570
+ </div>
571
+
572
+ <!-- 뉴스 탭 -->
573
+ <div id="news-content" class="tab-content active">
574
+ {% for article in analyzed_news %}
575
+ <div class="news-card">
576
+ <div class="news-header">
577
+ <div class="news-title">{{ loop.index }}. {{ article.title }}</div>
578
+ <div class="news-meta">
579
+ <span>📅 {{ article.date }}</span>
580
+ <span>📰 {{ article.source }}</span>
581
+ </div>
582
+ </div>
583
+
584
+ <div class="analysis-section">
585
+ <div class="analysis-item">
586
+ <span class="analysis-label">🎯 쉬운 요약</span>
587
+ <div class="analysis-content">{{ article.analysis.summary }}</div>
588
+ </div>
589
+
590
+ <div class="analysis-item">
591
+ <span class="analysis-label">💡 왜 중요할까?</span>
592
+ <div class="analysis-content">{{ article.analysis.significance }}</div>
593
+ </div>
594
+
595
+ <div class="analysis-item">
596
+ <span class="analysis-label">📊 영향도</span>
597
+ <span class="impact-level impact-{{ article.analysis.impact_level }}">
598
+ {{ article.analysis.impact_text }}
599
+ </span>
600
+ <div class="analysis-content" style="margin-top: 10px;">
601
+ {{ article.analysis.impact_description }}
602
+ </div>
603
+ </div>
604
+
605
+ <div class="analysis-item">
606
+ <span class="analysis-label">✅ 우리가 할 수 있는 것</span>
607
+ <div class="analysis-content">{{ article.analysis.action }}</div>
608
+ </div>
609
+ </div>
610
+
611
+ <a href="{{ article.url }}" target="_blank" class="news-link">
612
+ 🔗 전체 기사 읽어보기
613
+ </a>
614
+ </div>
615
+ {% endfor %}
616
+ </div>
617
+
618
+ <!-- 모델 탭 -->
619
+ <div id="models-content" class="tab-content">
620
+ <div class="model-grid">
621
+ {% for model in analyzed_models %}
622
+ <div class="model-card">
623
+ <div class="model-rank">{{ model.rank }}</div>
624
+ <div class="model-name">{{ model.name }}</div>
625
+ <div class="model-task">🏷️ {{ model.task }}</div>
626
+
627
+ <div class="model-stats">
628
+ <div class="model-stat-item">
629
+ <strong>📥 다운로드</strong><br>
630
+ {{ "{:,}".format(model.downloads) }}
631
+ </div>
632
+ <div class="model-stat-item">
633
+ <strong>❤️ 좋아요</strong><br>
634
+ {{ "{:,}".format(model.likes) }}
635
+ </div>
636
+ </div>
637
+
638
+ <div class="model-analysis">
639
+ <strong>🧠 AI 분석:</strong><br>
640
+ {{ model.analysis }}
641
+ </div>
642
+
643
+ <a href="{{ model.url }}" target="_blank" class="news-link">
644
+ 🔗 모델 페이지 방문
645
+ </a>
646
+ </div>
647
+ {% endfor %}
648
+ </div>
649
+
650
+ {% if analyzed_models|length == 0 %}
651
+ <div class="loading">
652
+ ⚠️ 모델 데이터를 불러오는 중...<br>
653
+ <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;">
654
+ 🔥 데이터 수집하기
655
+ </button>
656
+ </div>
657
+ {% endif %}
658
+ </div>
659
+
660
+ <!-- 스페이스 탭 -->
661
+ <div id="spaces-content" class="tab-content">
662
+ {% for space in analyzed_spaces %}
663
+ <div class="space-card">
664
+ <div class="space-header">
665
+ <div class="space-name">{{ space.rank }}. {{ space.name }}</div>
666
+ <span class="space-badge">트렌딩 {{ space.rank }}위</span>
667
+ </div>
668
+
669
+ <div class="space-description">
670
+ <strong>📝 설명:</strong> {{ space.description }}
671
+ </div>
672
+
673
+ <div class="space-analysis">
674
+ <strong>🎓 초등학생 설명:</strong><br>
675
+ {{ space.simple_explanation }}
676
+ </div>
677
+
678
+ {% if space.tech_stack %}
679
+ <div class="space-tech">
680
+ <strong style="width: 100%; margin-bottom: 5px;">🛠️ 사용 기술:</strong>
681
+ {% for tech in space.tech_stack %}
682
+ <span class="tech-tag">{{ tech }}</span>
683
+ {% endfor %}
684
+ </div>
685
+ {% endif %}
686
+
687
+ <a href="{{ space.url }}" target="_blank" class="news-link">
688
+ 🔗 스페이스 체험하기
689
+ </a>
690
+ </div>
691
+ {% endfor %}
692
+
693
+ {% if analyzed_spaces|length == 0 %}
694
+ <div class="loading">
695
+ ⚠️ 스페이스 데이터를 불러오는 중...<br>
696
+ <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;">
697
+ 🔥 데이터 수집하기
698
+ </button>
699
+ </div>
700
+ {% endif %}
701
+ </div>
702
+
703
+ <!-- 버튼 그룹 -->
704
+ <div class="button-group">
705
+ <button class="refresh-btn" onclick="location.reload()">
706
+ 🔄 페이지 새로고침
707
+ </button>
708
+ <button class="refresh-btn" onclick="location.href='/?refresh=true'" style="background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);">
709
+ 🔥 데이터 강제 갱신
710
+ </button>
711
+ </div>
712
+
713
+ <!-- 타임스탬프 -->
714
+ <div class="timestamp">
715
+ ⏰ 마지막 업데이트: {{ timestamp }}
716
+ </div>
717
+
718
+ <!-- 푸터 -->
719
+ <div class="footer">
720
+ <p>🤖 AI 뉴스 LLM 분석 시스템 v3.1 (Fireworks AI Edition)</p>
721
+ <p style="margin-top: 10px; font-size: 0.9em;">
722
+ 💾 SQLite DB 영구 저장 | 🤗 Hugging Face Trending API | 🔥 Powered by Fireworks AI (Qwen3-235B)
723
+ </p>
724
+ <p style="margin-top: 10px; font-size: 0.85em; color: #999;">
725
+ 데이터 출처: AI Times, Hugging Face | 분석: Fireworks AI LLM
726
+ </p>
727
+ </div>
728
+ </div>
729
+
730
+ <script>
731
+ function switchTab(tabName) {
732
+ // 모든 탭 비활성화
733
+ document.querySelectorAll('.tab').forEach(tab => {
734
+ tab.classList.remove('active');
735
+ });
736
+ document.querySelectorAll('.tab-content').forEach(content => {
737
+ content.classList.remove('active');
738
+ });
739
+
740
+ // 선택된 탭 활성화
741
+ event.target.classList.add('active');
742
+ document.getElementById(tabName + '-content').classList.add('active');
743
+ }
744
+
745
+ console.log('✅ AI 뉴스 LLM 분석 시스템 로드 완료 (Fireworks AI)');
746
+ </script>
747
+ </body>
748
+ </html>
749
+ """
750
+
751
+
752
+ # ============================================
753
+ # 데이터베이스 초기화
754
+ # ============================================
755
+
756
+ def init_database():
757
+ """SQLite 데이터베이스 초기화"""
758
+ conn = sqlite3.connect(DB_PATH)
759
+ cursor = conn.cursor()
760
+
761
+ # 뉴스 테이블
762
+ cursor.execute('''
763
+ CREATE TABLE IF NOT EXISTS news (
764
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
765
+ title TEXT NOT NULL,
766
+ url TEXT NOT NULL UNIQUE,
767
+ date TEXT,
768
+ source TEXT,
769
+ category TEXT,
770
+ summary TEXT,
771
+ significance TEXT,
772
+ impact_level TEXT,
773
+ impact_text TEXT,
774
+ impact_description TEXT,
775
+ action TEXT,
776
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
777
+ )
778
+ ''')
779
+
780
+ # 모델 테이블
781
+ cursor.execute('''
782
+ CREATE TABLE IF NOT EXISTS models (
783
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
784
+ name TEXT NOT NULL UNIQUE,
785
+ downloads INTEGER,
786
+ likes INTEGER,
787
+ task TEXT,
788
+ url TEXT,
789
+ analysis TEXT,
790
+ rank INTEGER,
791
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
792
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
793
+ )
794
+ ''')
795
+
796
+ # 스페이스 테이블
797
+ cursor.execute('''
798
+ CREATE TABLE IF NOT EXISTS spaces (
799
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
800
+ space_id TEXT NOT NULL UNIQUE,
801
+ name TEXT NOT NULL,
802
+ author TEXT,
803
+ title TEXT,
804
+ likes INTEGER,
805
+ url TEXT,
806
+ sdk TEXT,
807
+ simple_explanation TEXT,
808
+ tech_stack TEXT,
809
+ rank INTEGER,
810
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
811
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
812
+ )
813
+ ''')
814
+
815
+ conn.commit()
816
+ conn.close()
817
+ print("✅ 데이터베이스 초기화 완료")
818
+
819
+
820
+ def save_news_to_db(news_list: List[Dict]):
821
+ """뉴스 데이터를 DB에 저장"""
822
+ conn = sqlite3.connect(DB_PATH)
823
+ cursor = conn.cursor()
824
+
825
+ saved_count = 0
826
+ for news in news_list:
827
+ try:
828
+ cursor.execute('''
829
+ INSERT OR REPLACE INTO news
830
+ (title, url, date, source, category, summary, significance,
831
+ impact_level, impact_text, impact_description, action)
832
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
833
+ ''', (
834
+ news['title'],
835
+ news['url'],
836
+ news.get('date', ''),
837
+ news.get('source', ''),
838
+ news.get('category', ''),
839
+ news['analysis']['summary'],
840
+ news['analysis']['significance'],
841
+ news['analysis']['impact_level'],
842
+ news['analysis']['impact_text'],
843
+ news['analysis']['impact_description'],
844
+ news['analysis']['action']
845
+ ))
846
+ saved_count += 1
847
+ except sqlite3.IntegrityError:
848
+ pass
849
+
850
+ conn.commit()
851
+ conn.close()
852
+ print(f"✅ {saved_count}개 뉴스 DB 저장 완료")
853
+
854
+
855
+ def save_models_to_db(models_list: List[Dict]):
856
+ """모델 데이터를 DB에 저장"""
857
+ conn = sqlite3.connect(DB_PATH)
858
+ cursor = conn.cursor()
859
+
860
+ saved_count = 0
861
+ for model in models_list:
862
+ try:
863
+ cursor.execute('''
864
+ INSERT OR REPLACE INTO models
865
+ (name, downloads, likes, task, url, analysis, rank, updated_at)
866
+ VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
867
+ ''', (
868
+ model['name'],
869
+ model['downloads'],
870
+ model['likes'],
871
+ model['task'],
872
+ model['url'],
873
+ model['analysis'],
874
+ model['rank']
875
+ ))
876
+ saved_count += 1
877
+ except Exception as e:
878
+ print(f"⚠️ 모델 저장 오류: {e}")
879
+
880
+ conn.commit()
881
+ conn.close()
882
+ print(f"✅ {saved_count}개 모델 DB 저장 완료")
883
+
884
+
885
+ def save_spaces_to_db(spaces_list: List[Dict]):
886
+ """스페이스 데이터를 DB에 저장"""
887
+ conn = sqlite3.connect(DB_PATH)
888
+ cursor = conn.cursor()
889
+
890
+ saved_count = 0
891
+ for space in spaces_list:
892
+ try:
893
+ cursor.execute('''
894
+ INSERT OR REPLACE INTO spaces
895
+ (space_id, name, author, title, likes, url, sdk,
896
+ simple_explanation, tech_stack, rank, updated_at)
897
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
898
+ ''', (
899
+ space['space_id'],
900
+ space['name'],
901
+ space.get('author', ''),
902
+ space.get('title', ''),
903
+ space.get('likes', 0),
904
+ space['url'],
905
+ space.get('sdk', ''),
906
+ space['simple_explanation'],
907
+ json.dumps(space.get('tech_stack', [])),
908
+ space['rank']
909
+ ))
910
+ saved_count += 1
911
+ except Exception as e:
912
+ print(f"⚠️ 스페이스 저장 오류: {e}")
913
+
914
+ conn.commit()
915
+ conn.close()
916
+ print(f"✅ {saved_count}개 스페이스 DB 저장 완료")
917
+
918
+
919
+ def load_news_from_db() -> List[Dict]:
920
+ """DB에서 뉴스 로드"""
921
+ conn = sqlite3.connect(DB_PATH)
922
+ cursor = conn.cursor()
923
+
924
+ cursor.execute('''
925
+ SELECT title, url, date, source, category, summary, significance,
926
+ impact_level, impact_text, impact_description, action
927
+ FROM news ORDER BY created_at DESC LIMIT 50
928
+ ''')
929
+
930
+ news_list = []
931
+ for row in cursor.fetchall():
932
+ news_list.append({
933
+ 'title': row[0],
934
+ 'url': row[1],
935
+ 'date': row[2],
936
+ 'source': row[3],
937
+ 'category': row[4],
938
+ 'analysis': {
939
+ 'summary': row[5],
940
+ 'significance': row[6],
941
+ 'impact_level': row[7],
942
+ 'impact_text': row[8],
943
+ 'impact_description': row[9],
944
+ 'action': row[10]
945
+ }
946
+ })
947
+
948
+ conn.close()
949
+ return news_list
950
+
951
+
952
+ def load_models_from_db() -> List[Dict]:
953
+ """DB에서 모델 로드"""
954
+ conn = sqlite3.connect(DB_PATH)
955
+ cursor = conn.cursor()
956
+
957
+ cursor.execute('''
958
+ SELECT name, downloads, likes, task, url, analysis, rank
959
+ FROM models ORDER BY rank ASC LIMIT 30
960
+ ''')
961
+
962
+ models_list = []
963
+ for row in cursor.fetchall():
964
+ models_list.append({
965
+ 'name': row[0],
966
+ 'downloads': row[1],
967
+ 'likes': row[2],
968
+ 'task': row[3],
969
+ 'url': row[4],
970
+ 'analysis': row[5],
971
+ 'rank': row[6]
972
+ })
973
+
974
+ conn.close()
975
+ return models_list
976
+
977
+
978
+ def load_spaces_from_db() -> List[Dict]:
979
+ """DB에서 스페이스 로드"""
980
+ conn = sqlite3.connect(DB_PATH)
981
+ cursor = conn.cursor()
982
+
983
+ cursor.execute('''
984
+ SELECT space_id, name, author, title, likes, url, sdk,
985
+ simple_explanation, tech_stack, rank
986
+ FROM spaces ORDER BY rank ASC LIMIT 30
987
+ ''')
988
+
989
+ spaces_list = []
990
+ for row in cursor.fetchall():
991
+ spaces_list.append({
992
+ 'space_id': row[0],
993
+ 'name': row[1],
994
+ 'author': row[2],
995
+ 'title': row[3],
996
+ 'likes': row[4],
997
+ 'url': row[5],
998
+ 'sdk': row[6],
999
+ 'simple_explanation': row[7],
1000
+ 'tech_stack': json.loads(row[8]) if row[8] else [],
1001
+ 'rank': row[9],
1002
+ 'description': row[3]
1003
+ })
1004
+
1005
+ conn.close()
1006
+ return spaces_list
1007
+
1008
+
1009
+ # ============================================
1010
+ # Fireworks AI LLM 분석기 클래스
1011
+ # ============================================
1012
+
1013
+ class FireworksLLMAnalyzer:
1014
+ """Fireworks AI API를 사용한 LLM 분석기"""
1015
+
1016
+ def __init__(self, api_key: str = None):
1017
+ self.api_key = api_key or FIREWORKS_API_KEY
1018
+ self.api_url = FIREWORKS_API_URL
1019
+ self.model = "accounts/fireworks/models/qwen3-235b-a22b-instruct-2507"
1020
+
1021
+ if not self.api_key:
1022
+ print("⚠️ FIREWORKS_API_KEY가 설정되지 않았습니다. 샘플 분석을 사용합니다.")
1023
+ self.api_available = False
1024
+ else:
1025
+ self.api_available = True
1026
+ print("✅ Fireworks AI API 연결 준비 완료")
1027
+
1028
+ def call_llm(self, prompt: str, max_tokens: int = 2000) -> str:
1029
+ """Fireworks AI LLM 호출"""
1030
+ if not self.api_available:
1031
+ return ""
1032
+
1033
+ try:
1034
+ payload = {
1035
+ "model": self.model,
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": [
1043
+ {
1044
+ "role": "user",
1045
+ "content": prompt
1046
+ }
1047
+ ]
1048
+ }
1049
+
1050
+ headers = {
1051
+ "Accept": "application/json",
1052
+ "Content-Type": "application/json",
1053
+ "Authorization": f"Bearer {self.api_key}"
1054
+ }
1055
+
1056
+ response = requests.post(
1057
+ self.api_url,
1058
+ headers=headers,
1059
+ data=json.dumps(payload),
1060
+ timeout=30
1061
+ )
1062
+
1063
+ if response.status_code == 200:
1064
+ result = response.json()
1065
+ return result['choices'][0]['message']['content'].strip()
1066
+ else:
1067
+ print(f"⚠️ LLM API 오류: {response.status_code}")
1068
+ return ""
1069
+
1070
+ except Exception as e:
1071
+ print(f"⚠️ LLM 호출 오류: {e}")
1072
+ return ""
1073
+
1074
+ def analyze_news_simple(self, title: str, content: str = "") -> Dict:
1075
+ """뉴스 기사를 초등학생 수준으로 분석"""
1076
+
1077
+ prompt = f"""다음 AI 뉴스를 초등학생(10살)도 이해할 수 있게 분석해주세요.
1078
+
1079
+ 뉴스 제목: {title}
1080
+
1081
+ 다음 형식으로 JSON 응답해주세요:
1082
+ {{
1083
+ "summary": "초등학생이 이해할 수 있는 쉬운 요약 (2-3문장)",
1084
+ "significance": "이 뉴스가 왜 중요한지 쉽게 설명 (2문장)",
1085
+ "impact_level": "high 또는 medium 또는 low",
1086
+ "impact_text": "높음 또는 중간 또는 낮음",
1087
+ "impact_description": "영향도에 대한 설명 (1-2문장)",
1088
+ "action": "우리가 할 수 있는 것 (1-2문장)"
1089
+ }}
1090
+
1091
+ JSON만 출력하고 다른 설명은 하지 마세요."""
1092
+
1093
+ response = self.call_llm(prompt, max_tokens=1500)
1094
+
1095
+ if response:
1096
+ try:
1097
+ # JSON 파싱
1098
+ response = response.strip()
1099
+ if response.startswith("```json"):
1100
+ response = response[7:]
1101
+ if response.endswith("```"):
1102
+ response = response[:-3]
1103
+ response = response.strip()
1104
+
1105
+ analysis = json.loads(response)
1106
+ return analysis
1107
+ except json.JSONDecodeError as e:
1108
+ print(f"⚠️ JSON 파싱 오류: {e}")
1109
+ print(f"응답: {response[:200]}")
1110
+
1111
+ # 기본 분석 (API 실패 시)
1112
+ return self._get_fallback_news_analysis(title)
1113
+
1114
+ def _get_fallback_news_analysis(self, title: str) -> Dict:
1115
+ """API 실패 시 기본 분석"""
1116
+ analysis_templates = {
1117
+ "챗GPT": {
1118
+ "summary": "마이크로소프트(MS)라는 큰 회사가 챗GPT라는 AI를 너무 많은 사람들이 사용해서, 컴퓨터를 보관하는 큰 건물(데이터센터)이 부족하다고 말했어요.",
1119
+ "significance": "챗GPT가 정말 인기가 많다는 뜻이에요. 마치 너무 많은 친구들이 한 게임기를 쓰려고 하는 것과 비슷해요.",
1120
+ "impact_level": "high",
1121
+ "impact_text": "높음",
1122
+ "impact_description": "AI 기술이 빠르게 발전하고 있고, 많은 사람들이 사용하고 있다는 중요한 신호예요.",
1123
+ "action": "챗GPT 같은 AI 도구를 배워보세요. 숙제를 도와달라고 하거나, 모르는 것을 물어볼 수 있어요!"
1124
+ },
1125
+ "GPU": {
1126
+ "summary": "미국이 아랍에미리트(UAE)라는 나라에 GPU라는 특별한 컴퓨터 부품을 팔 수 있게 허락했어요. GPU는 AI를 만드는 데 꼭 필요한 부품이에요.",
1127
+ "significance": "GPU는 AI의 두뇌 같은 거예요. 이걸 팔 수 있게 되면 더 많은 나라에서 AI를 만들 수 있어요.",
1128
+ "impact_level": "medium",
1129
+ "impact_text": "중간",
1130
+ "impact_description": "AI 기술이 더 많은 나라로 퍼질 수 있게 되었어요.",
1131
+ "action": "컴퓨터가 어떻게 작동하는지 관심을 가져보세요. GPU가 무엇인지 검색해보는 것도 좋아요!"
1132
+ },
1133
+ "소라": {
1134
+ "summary": "오픈AI가 만든 '소라'라는 AI 앱이 엄청 빠르게 인기를 얻었어요. 100만 명이 다운로드하는 데 챗GPT보다 더 빨랐대요!",
1135
+ "significance": "사람들이 비디오를 만드는 AI에 정말 관심이 많다는 뜻이에요.",
1136
+ "impact_level": "high",
1137
+ "impact_text": "높음",
1138
+ "impact_description": "앞으로 누구나 쉽게 멋진 비디오를 만들 수 있게 될 거예요.",
1139
+ "action": "소라를 써보고, 상상한 것을 비디오로 만들어보세요. 창의력을 발휘할 수 있어요!"
1140
+ }
1141
+ }
1142
+
1143
+ for keyword, template in analysis_templates.items():
1144
+ if keyword.lower() in title.lower():
1145
+ return template
1146
+
1147
+ return {
1148
+ "summary": f"'{title}'라는 AI 관련 뉴스가 나왔어요. AI 기술이 계속 발전하고 있다는 소식이에요.",
1149
+ "significance": "AI는 우리 생활을 더 편리하게 만들어주는 기술이에요.",
1150
+ "impact_level": "medium",
1151
+ "impact_text": "중간",
1152
+ "impact_description": "AI 기술의 발전은 우리 미래에 중요한 영향을 줄 거예요.",
1153
+ "action": "AI에 대해 더 알아보고, AI를 활용하는 방법을 배워보세요!"
1154
+ }
1155
+
1156
+ def analyze_model(self, model_name: str, task: str, downloads: int) -> str:
1157
+ """허깅페이스 모델 분석"""
1158
+
1159
+ prompt = f"""다음 허깅페이스 AI 모델을 초등학생(10살)이 이해할 수 있게 설명해주세요.
1160
+
1161
+ 모델명: {model_name}
1162
+ 태스크: {task}
1163
+ 다운로드 수: {downloads:,}
1164
+
1165
+ 2-3문장으로 이 모델이 무엇을 하는지, 왜 인기가 많은지 쉽게 설명해주세요.
1166
+ 설명만 작성하고 다른 내용은 쓰지 마세요."""
1167
+
1168
+ response = self.call_llm(prompt, max_tokens=300)
1169
+
1170
+ if response:
1171
+ return response
1172
+
1173
+ # 기본 분석
1174
+ task_explanations = {
1175
+ "text-generation": "글을 자동으로 만들어주는",
1176
+ "image-to-text": "사진을 보고 설명을 써주는",
1177
+ "text-to-image": "글을 읽고 그림을 그려주는",
1178
+ "translation": "다른 언어로 번역해주는",
1179
+ "question-answering": "질문에 답해주는",
1180
+ "summarization": "긴 글을 짧게 요약해주는",
1181
+ "text-classification": "글을 분류해주는",
1182
+ "token-classification": "단어를 분석해주는",
1183
+ "fill-mask": "빈칸을 채워주는"
1184
+ }
1185
+
1186
+ task_desc = task_explanations.get(task, "특별한 기능을 하는")
1187
+
1188
+ if downloads > 10000000:
1189
+ popularity = "엄청나게 많은"
1190
+ elif downloads > 1000000:
1191
+ popularity = "아주 많은"
1192
+ elif downloads > 100000:
1193
+ popularity = "많은"
1194
+ else:
1195
+ popularity = "어느 정도"
1196
+
1197
+ return f"이 모델은 {task_desc} AI예요. {popularity} 사람들이 다운로드해서 사용하고 있어요. {model_name.split('/')[-1]}라는 이름으로 유명해요!"
1198
+
1199
+ def analyze_space(self, space_name: str, description: str) -> Dict:
1200
+ """허깅페이스 스페이스 분석"""
1201
+
1202
+ prompt = f"""다음 허깅페이스 스페이스를 초등학생(10살)이 이해할 수 있게 설명해주세요.
1203
+
1204
+ 스페이스명: {space_name}
1205
+ 설명: {description}
1206
+
1207
+ 다음 형식으로 JSON 응답해주세요:
1208
+ {{
1209
+ "simple_explanation": "이 스페이스가 무엇을 하는지 쉽게 설명 (2-3문장)",
1210
+ "tech_stack": ["기술1", "기술2", "기술3"]
1211
+ }}
1212
+
1213
+ JSON만 출력하고 다른 설명은 하지 마세요."""
1214
+
1215
+ response = self.call_llm(prompt, max_tokens=500)
1216
+
1217
+ if response:
1218
+ try:
1219
+ response = response.strip()
1220
+ if response.startswith("```json"):
1221
+ response = response[7:]
1222
+ if response.endswith("```"):
1223
+ response = response[:-3]
1224
+ response = response.strip()
1225
+
1226
+ analysis = json.loads(response)
1227
+ return analysis
1228
+ except json.JSONDecodeError:
1229
+ pass
1230
+
1231
+ # 기본 분석
1232
+ return {
1233
+ "simple_explanation": f"{space_name}는 웹브라우저에서 바로 AI를 체험해볼 수 있는 곳이에요. 설치 없이도 사용할 수 있어서 편리해요! 마치 온라인 게임처럼 바로 접속해서 AI를 사용할 수 있답니다.",
1234
+ "tech_stack": ["Python", "Gradio", "Transformers", "PyTorch"]
1235
+ }
1236
+
1237
+
1238
+ # ============================================
1239
+ # 고급 분석기 클래스
1240
+ # ============================================
1241
+
1242
+ class AdvancedAIAnalyzer:
1243
+ """LLM 기반 고급 AI 뉴스 분석기 (Fireworks AI)"""
1244
+
1245
+ def __init__(self):
1246
+ self.llm_analyzer = FireworksLLMAnalyzer()
1247
+ self.huggingface_data = {
1248
+ "models": [],
1249
+ "spaces": []
1250
+ }
1251
+ self.news_data = []
1252
+
1253
+ def fetch_huggingface_models(self, limit: int = 30) -> List[Dict]:
1254
+ """허깅페이스 트렌딩 모델 30개 수집 (실제 API)"""
1255
+ print(f"🤗 허깅페이스 트렌딩 모델 {limit}개 수집 중...")
1256
+
1257
+ models_list = []
1258
+
1259
+ try:
1260
+ api = HfApi()
1261
+ models = list(api.list_models(
1262
+ sort="trending",
1263
+ direction=-1,
1264
+ limit=limit
1265
+ ))
1266
+
1267
+ print(f"📊 API에서 {len(models)}개 모델 받음")
1268
+
1269
+ for idx, model in enumerate(models[:limit], 1):
1270
+ try:
1271
+ model_info = {
1272
+ 'name': model.id,
1273
+ 'downloads': getattr(model, 'downloads', 0) or 0,
1274
+ 'likes': getattr(model, 'likes', 0) or 0,
1275
+ 'task': getattr(model, 'pipeline_tag', 'N/A') or 'N/A',
1276
+ 'url': f"https://huggingface.co/{model.id}",
1277
+ 'rank': idx
1278
+ }
1279
+
1280
+ # LLM 분석 추가
1281
+ print(f" 🧠 모델 {idx} LLM 분석 중...")
1282
+ model_info['analysis'] = self.llm_analyzer.analyze_model(
1283
+ model_info['name'],
1284
+ model_info['task'],
1285
+ model_info['downloads']
1286
+ )
1287
+
1288
+ models_list.append(model_info)
1289
+
1290
+ if idx % 5 == 0:
1291
+ print(f" ✓ {idx}개 모델 처리 완료...")
1292
+ time.sleep(0.5) # Rate limit 방지
1293
+
1294
+ except Exception as e:
1295
+ print(f" ⚠️ 모델 {idx} 처리 오류: {e}")
1296
+ continue
1297
+
1298
+ print(f"✅ {len(models_list)}개 트렌딩 모델 수집 완료")
1299
+
1300
+ if models_list:
1301
+ save_models_to_db(models_list)
1302
+
1303
+ return models_list
1304
+
1305
+ except Exception as e:
1306
+ print(f"❌ 모델 수집 오류: {e}")
1307
+ print("💾 DB에서 이전 데이터 로드 시도...")
1308
+ return load_models_from_db()
1309
+
1310
+ def fetch_huggingface_spaces(self, limit: int = 30) -> List[Dict]:
1311
+ """허깅페이스 트렌딩 스페이스 30개 수집 (실제 API)"""
1312
+ print(f"🚀 허깅페이스 트렌딩 스페이스 {limit}개 수집 중...")
1313
+
1314
+ spaces_list = []
1315
+
1316
+ try:
1317
+ api = HfApi()
1318
+ spaces = list(api.list_spaces(
1319
+ sort="trending",
1320
+ direction=-1,
1321
+ limit=limit
1322
+ ))
1323
+
1324
+ print(f"📊 API에서 {len(spaces)}개 스페이스 받음")
1325
+
1326
+ for idx, space in enumerate(spaces[:limit], 1):
1327
+ try:
1328
+ space_info = {
1329
+ 'space_id': space.id,
1330
+ 'name': space.id.split('/')[-1] if '/' in space.id else space.id,
1331
+ 'author': space.author,
1332
+ 'title': getattr(space, 'title', space.id) or space.id,
1333
+ 'likes': getattr(space, 'likes', 0) or 0,
1334
+ 'url': f"https://huggingface.co/spaces/{space.id}",
1335
+ 'sdk': getattr(space, 'sdk', 'gradio') or 'gradio',
1336
+ 'rank': idx
1337
+ }
1338
+
1339
+ # LLM 분석 추가
1340
+ print(f" 🧠 스페이스 {idx} LLM 분석 중...")
1341
+ space_analysis = self.llm_analyzer.analyze_space(
1342
+ space_info['name'],
1343
+ space_info['title']
1344
+ )
1345
+
1346
+ space_info['simple_explanation'] = space_analysis['simple_explanation']
1347
+ space_info['tech_stack'] = space_analysis['tech_stack']
1348
+ space_info['description'] = space_info['title']
1349
+
1350
+ spaces_list.append(space_info)
1351
+
1352
+ if idx % 5 == 0:
1353
+ print(f" ✓ {idx}개 스페이스 처리 완료...")
1354
+ time.sleep(0.5) # Rate limit 방지
1355
+
1356
+ except Exception as e:
1357
+ print(f" ⚠️ 스페이스 {idx} 처리 오류: {e}")
1358
+ continue
1359
+
1360
+ print(f"✅ {len(spaces_list)}개 트렌딩 스페이스 수집 완료")
1361
+
1362
+ if spaces_list:
1363
+ save_spaces_to_db(spaces_list)
1364
+
1365
+ return spaces_list
1366
+
1367
+ except Exception as e:
1368
+ print(f"❌ 스페이스 수집 오류: {e}")
1369
+ print("💾 DB에서 이전 데이터 로드 시도...")
1370
+ return load_spaces_from_db()
1371
+
1372
+ def create_sample_news(self) -> List[Dict]:
1373
+ """오늘의 AI 뉴스 샘플"""
1374
+ sample_news = [
1375
+ {
1376
+ 'title': 'MS "챗GPT 수요 폭증으로 데이터센터 부족...2026년까지 지속"',
1377
+ 'url': 'https://www.aitimes.com/news/articleView.html?idxno=203055',
1378
+ 'date': '10-10 15:10',
1379
+ 'source': 'AI Times'
1380
+ },
1381
+ {
1382
+ 'title': '미국, UAE에 GPU 판매 일부 승인...엔비디아 시총 5조달러 눈앞',
1383
+ 'url': 'https://www.aitimes.com/news/articleView.html?idxno=203053',
1384
+ 'date': '10-10 14:46',
1385
+ 'source': 'AI Times'
1386
+ },
1387
+ {
1388
+ 'title': '소라, 챗GPT보다 빨리 100만 다운로드 돌파',
1389
+ 'url': 'https://www.aitimes.com/news/articleView.html?idxno=203045',
1390
+ 'date': '10-10 12:55',
1391
+ 'source': 'AI Times'
1392
+ }
1393
+ ]
1394
+
1395
+ return sample_news
1396
+
1397
+ def analyze_all_news(self) -> List[Dict]:
1398
+ """모든 뉴스에 LLM 분석 추가"""
1399
+ print("📰 뉴스 LLM 분석 시작...")
1400
+
1401
+ news = self.create_sample_news()
1402
+ analyzed_news = []
1403
+
1404
+ for idx, article in enumerate(news, 1):
1405
+ print(f" 🧠 뉴스 {idx}/{len(news)} LLM 분석 중...")
1406
+
1407
+ analysis = self.llm_analyzer.analyze_news_simple(
1408
+ article['title'],
1409
+ ""
1410
+ )
1411
+
1412
+ article['analysis'] = analysis
1413
+ analyzed_news.append(article)
1414
+
1415
+ time.sleep(0.5) # Rate limit 방지
1416
+
1417
+ print(f"✅ {len(analyzed_news)}개 뉴스 분석 완료")
1418
+
1419
+ save_news_to_db(analyzed_news)
1420
+
1421
+ return analyzed_news
1422
+
1423
+ def get_all_data(self, force_refresh: bool = False) -> Dict:
1424
+ """모든 데이터 수집 및 분석"""
1425
+ print("\n" + "="*60)
1426
+ print("🚀 AI 뉴스 & 허깅페이스 LLM 분석 시작 (Fireworks AI)")
1427
+ print("="*60 + "\n")
1428
+
1429
+ if force_refresh:
1430
+ print("🔄 강제 새로고침 모드: 모든 데이터 새로 수집")
1431
+ analyzed_news = self.analyze_all_news()
1432
+ analyzed_models = self.fetch_huggingface_models(30)
1433
+ analyzed_spaces = self.fetch_huggingface_spaces(30)
1434
+ else:
1435
+ print("💾 DB 우선 로드 모드")
1436
+
1437
+ analyzed_news = load_news_from_db()
1438
+ if not analyzed_news:
1439
+ print("📰 DB에 뉴스 없음 → 새로 수집")
1440
+ analyzed_news = self.analyze_all_news()
1441
+ else:
1442
+ print(f"✅ DB에서 {len(analyzed_news)}개 뉴스 로드")
1443
+
1444
+ analyzed_models = load_models_from_db()
1445
+ if not analyzed_models:
1446
+ print("🤗 DB에 모델 없음 → 새로 수집")
1447
+ analyzed_models = self.fetch_huggingface_models(30)
1448
+ else:
1449
+ print(f"✅ DB에서 {len(analyzed_models)}개 모델 로드")
1450
+
1451
+ analyzed_spaces = load_spaces_from_db()
1452
+ if not analyzed_spaces:
1453
+ print("🚀 DB에 스페이스 없음 → 새로 수집")
1454
+ analyzed_spaces = self.fetch_huggingface_spaces(30)
1455
+ else:
1456
+ print(f"✅ DB에서 {len(analyzed_spaces)}개 스페이스 로드")
1457
+
1458
+ stats = {
1459
+ 'total_news': len(analyzed_news),
1460
+ 'hf_models': len(analyzed_models),
1461
+ 'hf_spaces': len(analyzed_spaces),
1462
+ 'llm_analyses': len(analyzed_news) + len(analyzed_models) + len(analyzed_spaces)
1463
+ }
1464
+
1465
+ print(f"\n✅ 전체 분석 완료: {stats['llm_analyses']}개 항목")
1466
+ print(f" 📰 뉴스: {stats['total_news']}개")
1467
+ print(f" 🤗 모델: {stats['hf_models']}개")
1468
+ print(f" 🚀 스페이스: {stats['hf_spaces']}개")
1469
+
1470
+ return {
1471
+ 'analyzed_news': analyzed_news,
1472
+ 'analyzed_models': analyzed_models,
1473
+ 'analyzed_spaces': analyzed_spaces,
1474
+ 'stats': stats,
1475
+ 'timestamp': datetime.now().strftime('%Y년 %m월 %d일 %H:%M:%S')
1476
+ }
1477
+
1478
+
1479
+ # ============================================
1480
+ # Flask 라우트
1481
+ # ============================================
1482
+
1483
+ @app.route('/')
1484
+ def index():
1485
+ """메인 페이지"""
1486
+ try:
1487
+ force_refresh = request.args.get('refresh', 'false').lower() == 'true'
1488
+
1489
+ analyzer = AdvancedAIAnalyzer()
1490
+ data = analyzer.get_all_data(force_refresh=force_refresh)
1491
+ return render_template_string(HTML_TEMPLATE, **data)
1492
+ except Exception as e:
1493
+ import traceback
1494
+ error_detail = traceback.format_exc()
1495
+ return f"""
1496
+ <html>
1497
+ <body style="font-family: Arial; padding: 50px; text-align: center;">
1498
+ <h1 style="color: #e74c3c;">⚠️ 오류 발생</h1>
1499
+ <p>{str(e)}</p>
1500
+ <pre style="text-align: left; background: #f5f5f5; padding: 20px; border-radius: 5px;">
1501
+ {error_detail}
1502
+ </pre>
1503
+ <button onclick="location.href='/'" style="padding: 10px 20px; margin: 10px;">
1504
+ 🔄 새로고침
1505
+ </button>
1506
+ <button onclick="location.href='/?refresh=true'" style="padding: 10px 20px; margin: 10px; background: #ff6b6b; color: white; border: none; border-radius: 5px;">
1507
+ 🔥 강제 갱신
1508
+ </button>
1509
+ </body>
1510
+ </html>
1511
+ """, 500
1512
+
1513
+
1514
+ @app.route('/api/data')
1515
+ def api_data():
1516
+ """JSON API"""
1517
+ try:
1518
+ force_refresh = request.args.get('refresh', 'false').lower() == 'true'
1519
+ analyzer = AdvancedAIAnalyzer()
1520
+ data = analyzer.get_all_data(force_refresh=force_refresh)
1521
+ return jsonify({
1522
+ 'success': True,
1523
+ 'data': data
1524
+ })
1525
+ except Exception as e:
1526
+ return jsonify({
1527
+ 'success': False,
1528
+ 'error': str(e)
1529
+ }), 500
1530
+
1531
+
1532
+ @app.route('/api/refresh')
1533
+ def api_refresh():
1534
+ """강제 새로고침 API"""
1535
+ try:
1536
+ analyzer = AdvancedAIAnalyzer()
1537
+ data = analyzer.get_all_data(force_refresh=True)
1538
+ return jsonify({
1539
+ 'success': True,
1540
+ 'message': '데이터가 성공적으로 갱신되었습니다',
1541
+ 'stats': data['stats']
1542
+ })
1543
+ except Exception as e:
1544
+ return jsonify({
1545
+ 'success': False,
1546
+ 'error': str(e)
1547
+ }), 500
1548
+
1549
+
1550
+ @app.route('/health')
1551
+ def health():
1552
+ """헬스 체크"""
1553
+ try:
1554
+ conn = sqlite3.connect(DB_PATH)
1555
+ cursor = conn.cursor()
1556
+ cursor.execute("SELECT COUNT(*) FROM news")
1557
+ news_count = cursor.fetchone()[0]
1558
+ cursor.execute("SELECT COUNT(*) FROM models")
1559
+ models_count = cursor.fetchone()[0]
1560
+ cursor.execute("SELECT COUNT(*) FROM spaces")
1561
+ spaces_count = cursor.fetchone()[0]
1562
+ conn.close()
1563
+
1564
+ return jsonify({
1565
+ "status": "healthy",
1566
+ "service": "AI News LLM Analyzer (Fireworks AI)",
1567
+ "version": "3.1.0",
1568
+ "llm_provider": "Fireworks AI (Qwen3-235B)",
1569
+ "database": {
1570
+ "connected": True,
1571
+ "news_count": news_count,
1572
+ "models_count": models_count,
1573
+ "spaces_count": spaces_count
1574
+ },
1575
+ "timestamp": datetime.now().isoformat()
1576
+ })
1577
+ except Exception as e:
1578
+ return jsonify({
1579
+ "status": "unhealthy",
1580
+ "error": str(e)
1581
+ }), 500
1582
+
1583
+
1584
+ # ============================================
1585
+ # 메인 실행
1586
+ # ============================================
1587
+
1588
+ if __name__ == '__main__':
1589
+ port = int(os.environ.get('PORT', 7860))
1590
+
1591
+ print(f"""
1592
+ ╔════════════════════════════════════════════════════════════╗
1593
+ ║ ║
1594
+ ║ 🤖 AI 뉴스 & 허깅페이스 LLM 분석 웹앱 v3.1 ║
1595
+ ║ 🔥 Powered by Fireworks AI (Qwen3-235B) ║
1596
+ ║ ║
1597
+ ╚════════════════════════════════════════════════════════════╝
1598
+
1599
+ ✨ 주요 기능:
1600
+ • 🔥 Fireworks AI LLM으로 실제 분석
1601
+ • 💾 SQLite DB 영구 스토리지
1602
+ • 📰 뉴스 초등학생 수준 분석
1603
+ • 🤗 허깅페이스 트렌딩 모델 TOP 30
1604
+ • 🚀 허깅페이스 트렌딩 스페이스 TOP 30
1605
+ • 🎨 탭 UI (뉴스/모델/스페이스)
1606
+
1607
+ 🚀 서버 정보:
1608
+ 📍 메인: http://localhost:{port}
1609
+ 🔄 강제갱신: http://localhost:{port}/?refresh=true
1610
+ 📊 API: http://localhost:{port}/api/data
1611
+ 🔥 새로고침 API: http://localhost:{port}/api/refresh
1612
+ 💚 Health: http://localhost:{port}/health
1613
+
1614
+ 💾 데이터베이스: {DB_PATH}
1615
+ 🔑 API 키 상태: {'✅ 설정됨' if FIREWORKS_API_KEY else '⚠️ 미설정 (샘플 분석 사용)'}
1616
+
1617
+ 초기화 중...
1618
+ """)
1619
+
1620
+ try:
1621
+ init_database()
1622
+ except Exception as e:
1623
+ print(f"❌ DB 초기화 오류: {e}")
1624
+ sys.exit(1)
1625
+
1626
+ print("\n✅ 서버 준비 완료!")
1627
+ print("브라우저에서 위 URL을 열어주세요!")
1628
+ print("종료: Ctrl+C\n")
1629
+
1630
+ try:
1631
+ app.run(
1632
+ host='0.0.0.0',
1633
+ port=port,
1634
+ debug=False,
1635
+ threaded=True
1636
+ )
1637
+ except KeyboardInterrupt:
1638
+ print("\n\n👋 서버 종료!")
1639
+ sys.exit(0)
1640
+ except Exception as e:
1641
+ print(f"\n❌ 서버 오류: {e}")
1642
+ sys.exit(1)