ginipick commited on
Commit
3d9dfc6
Β·
verified Β·
1 Parent(s): 7cfd214

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +660 -206
app.py CHANGED
@@ -1,30 +1,513 @@
1
  # -*- coding: utf-8 -*-
2
  """
3
- AI λ‰΄μŠ€ & ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© 뢄석 μ‹œμŠ€ν…œ (μ‹€ν–‰ κ°€λŠ₯ 버전)
 
 
 
 
 
 
 
 
 
4
  """
5
 
 
6
  import requests
7
  from bs4 import BeautifulSoup
8
  import json
9
  from datetime import datetime
10
- from typing import List, Dict
11
- import time
 
 
 
 
 
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  class AINewsAnalyzer:
15
- def __init__(self, fireworks_api_key: str = None, brave_api_key: str = None):
 
 
16
  """
17
- API ν‚€λŠ” μ„ νƒμ‚¬ν•­μž…λ‹ˆλ‹€. 없어도 κΈ°λ³Έ κΈ°λŠ₯은 λ™μž‘ν•©λ‹ˆλ‹€.
 
 
18
  """
19
- self.fireworks_api_key = fireworks_api_key
20
- self.brave_api_key = brave_api_key
21
 
22
  # λ‰΄μŠ€ μΉ΄ν…Œκ³ λ¦¬ μ •μ˜
23
  self.categories = {
24
- "산업동ν–₯": ["μ‚°μ—…", "κΈ°μ—…", "투자", "인수", "νŒŒνŠΈλ„ˆμ‹­", "μ‹œμž₯", "MS", "ꡬ글", "μ•„λ§ˆμ‘΄"],
25
- "κΈ°μˆ ν˜μ‹ ": ["기술", "λͺ¨λΈ", "μ•Œκ³ λ¦¬μ¦˜", "개발", "연ꡬ", "λ…Όλ¬Έ", "μ‚Όμ„±"],
26
- "μ œν’ˆμΆœμ‹œ": ["μΆœμ‹œ", "곡개", "λ°œν‘œ", "μ„œλΉ„μŠ€", "μ œν’ˆ", "μ±—GPT", "μ†ŒλΌ"],
27
- "μ •μ±…κ·œμ œ": ["규제", "μ •μ±…", "법", "μ •λΆ€", "제재", "EU"],
28
  "λ³΄μ•ˆμ΄μŠˆ": ["λ³΄μ•ˆ", "취약점", "ν•΄ν‚Ή", "μœ„ν—˜", "ν”„λΌμ΄λ²„μ‹œ"],
29
  }
30
 
@@ -36,13 +519,15 @@ class AINewsAnalyzer:
36
  self.news_data = []
37
 
38
  def fetch_huggingface_trending(self) -> Dict:
39
- """ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© 정보 μˆ˜μ§‘"""
40
  print("πŸ€— ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© 정보 μˆ˜μ§‘ 쀑...")
41
 
42
  try:
43
- # λͺ¨λΈ νŠΈλ Œλ”© API
44
  models_url = "https://huggingface.co/api/models"
45
- params = {'sort': 'trending', 'limit': 30}
 
 
 
46
 
47
  response = requests.get(models_url, params=params, timeout=15)
48
 
@@ -65,7 +550,7 @@ class AINewsAnalyzer:
65
  except Exception as e:
66
  print(f"❌ λͺ¨λΈ μˆ˜μ§‘ 였λ₯˜: {e}")
67
 
68
- # 슀페이슀 μƒ˜ν”Œ 데이터 (μ‹€μ œ 크둀링은 λ³΅μž‘ν•˜λ―€λ‘œ)
69
  sample_spaces = [
70
  {"name": "Wan2.2-5B", "title": "κ³ ν’ˆμ§ˆ λΉ„λ””μ˜€ 생성", "url": "https://huggingface.co/spaces/"},
71
  {"name": "FLUX-Image", "title": "ν…μŠ€νŠΈβ†’μ΄λ―Έμ§€ 생성", "url": "https://huggingface.co/spaces/"},
@@ -73,7 +558,6 @@ class AINewsAnalyzer:
73
  ]
74
 
75
  self.huggingface_data['spaces'] = sample_spaces
76
- print(f"βœ… {len(self.huggingface_data['spaces'])}개 μƒ˜ν”Œ 슀페이슀 좔가됨")
77
 
78
  return self.huggingface_data
79
 
@@ -169,203 +653,173 @@ class AINewsAnalyzer:
169
 
170
  return news_list
171
 
172
- def analyze_with_qwen(self, text: str, instruction: str) -> str:
173
- """Fireworks AI Qwen을 μ‚¬μš©ν•œ 뢄석 (API ν‚€ ν•„μš”)"""
174
- if not self.fireworks_api_key:
175
- return "⚠️ Fireworks API ν‚€κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. 뢄석을 κ±΄λ„ˆλœλ‹ˆλ‹€."
 
176
 
177
- url = "https://api.fireworks.ai/inference/v1/chat/completions"
 
178
 
179
- payload = {
180
- "model": "accounts/fireworks/models/qwen3-235b-a22b-instruct-2507",
181
- "max_tokens": 4096,
182
- "temperature": 0.6,
183
- "messages": [
184
- {"role": "system", "content": "당신은 AI λ‰΄μŠ€λ₯Ό μ΄ˆλ“±ν•™μƒλ„ 이해할 수 있게 μ‰½κ²Œ μ„€λͺ…ν•˜λŠ” μ „λ¬Έκ°€μž…λ‹ˆλ‹€."},
185
- {"role": "user", "content": f"{instruction}\n\nλ‰΄μŠ€: {text}"}
186
- ]
187
- }
188
 
189
- headers = {
190
- "Accept": "application/json",
191
- "Content-Type": "application/json",
192
- "Authorization": f"Bearer {self.fireworks_api_key}"
 
 
193
  }
194
 
195
- try:
196
- response = requests.post(url, headers=headers, data=json.dumps(payload), timeout=30)
197
-
198
- if response.status_code == 200:
199
- result = response.json()
200
- return result['choices'][0]['message']['content']
201
- else:
202
- return f"⚠️ API 였λ₯˜: {response.status_code}"
203
-
204
- except Exception as e:
205
- return f"⚠️ 뢄석 였λ₯˜: {str(e)}"
206
-
207
- def generate_report(self, news_list: List[Dict], analyze_news: bool = False) -> str:
208
- """μ’…ν•© 리포트 생성"""
209
- report = []
210
- report.append("=" * 80)
211
- report.append("πŸ“Š AI λ‰΄μŠ€ & ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© μ’…ν•© 리포트")
212
- report.append(f"πŸ“… μƒμ„±μΌμ‹œ: {datetime.now().strftime('%Yλ…„ %mμ›” %d일 %H:%M')}")
213
- report.append("=" * 80)
214
- report.append("")
215
-
216
- # 1. μΉ΄ν…Œκ³ λ¦¬λ³„ λ‰΄μŠ€
217
- report.append("πŸ“° === AI λ‰΄μŠ€ 뢄석 (μΉ΄ν…Œκ³ λ¦¬λ³„) ===")
218
- report.append("")
219
-
220
- categorized_news = {}
221
- for news in news_list:
222
- category = news.get('category', '기타')
223
- if category not in categorized_news:
224
- categorized_news[category] = []
225
- categorized_news[category].append(news)
226
-
227
- for category, articles in categorized_news.items():
228
- report.append(f"πŸ“Œ [{category}] ({len(articles)}건)")
229
- report.append("-" * 80)
230
-
231
- for i, article in enumerate(articles, 1):
232
- report.append(f"{i}. {article['title']}")
233
- report.append(f" πŸ”— {article['url']}")
234
- report.append(f" πŸ“… {article.get('date', 'N/A')}")
235
-
236
- # LLM 뢄석 (API ν‚€κ°€ 있고 ν™œμ„±ν™”λœ 경우만)
237
- if analyze_news and self.fireworks_api_key and i <= 2:
238
- print(f"πŸ€– LLM 뢄석 쀑: {article['title'][:50]}...")
239
- instruction = "이 λ‰΄μŠ€λ₯Ό μ΄ˆλ“±ν•™μƒλ„ 이해할 수 있게 2-3λ¬Έμž₯으둜 μ„€λͺ…ν•˜κ³ , μ™œ μ€‘μš”ν•œμ§€ 1λ¬Έμž₯, 행동 μ§€μΉ¨ 1-2개λ₯Ό μ•Œλ €μ£Όμ„Έμš”."
240
- analysis = self.analyze_with_qwen(article['title'], instruction)
241
- report.append(f"\n πŸ€– AI 뢄석: {analysis}")
242
-
243
- report.append("")
244
-
245
- report.append("")
246
-
247
- # 2. ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”©
248
- report.append("πŸ€— === ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© TOP 30 ===")
249
- report.append("")
250
-
251
- # λͺ¨λΈ
252
- if self.huggingface_data['models']:
253
- report.append("πŸ”₯ νŠΈλ Œλ”© λͺ¨λΈ TOP 10")
254
- report.append("-" * 80)
255
- for i, model in enumerate(self.huggingface_data['models'][:10], 1):
256
- report.append(f"{i:2d}. {model['name']}")
257
- report.append(f" πŸ“Š λ‹€μš΄λ‘œλ“œ: {model['downloads']:,} | ❀️ {model['likes']:,}")
258
- report.append(f" 🏷️ {model['task']}")
259
- report.append(f" πŸ”— {model['url']}")
260
- report.append("")
261
- else:
262
- report.append("⚠️ λͺ¨λΈ 데이터 μˆ˜μ§‘ μ‹€νŒ¨")
263
-
264
- report.append("")
265
-
266
- # 슀페이슀
267
- if self.huggingface_data['spaces']:
268
- report.append("πŸš€ νŠΈλ Œλ”© 슀페이슀 μƒ˜ν”Œ")
269
- report.append("-" * 80)
270
- for i, space in enumerate(self.huggingface_data['spaces'], 1):
271
- report.append(f"{i}. {space['name']}")
272
- report.append(f" πŸ“ {space['title']}")
273
- report.append(f" πŸ”— {space['url']}")
274
- report.append("")
275
-
276
- # 3. μ’…ν•© μš”μ•½
277
- report.append("=" * 80)
278
- report.append("πŸ“ˆ μ’…ν•© μš”μ•½")
279
- report.append("=" * 80)
280
- report.append(f"β€’ 총 λ‰΄μŠ€: {len(news_list)}건")
281
- report.append(f"β€’ μΉ΄ν…Œκ³ λ¦¬: {len(categorized_news)}개")
282
- report.append(f"β€’ νŠΈλ Œλ”© λͺ¨λΈ: {len(self.huggingface_data['models'])}개")
283
- report.append(f"β€’ νŠΈλ Œλ”© 슀페이슀: {len(self.huggingface_data['spaces'])}개")
284
- report.append("")
285
- report.append("βœ… 뢄석 μ™„λ£Œ!")
286
- report.append("")
287
-
288
- return '\n'.join(report)
289
-
290
- def run(self, use_llm: bool = False) -> str:
291
- """전체 뢄석 μ‹€ν–‰"""
292
- print("=" * 80)
293
- print("πŸš€ AI λ‰΄μŠ€ & ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© 뢄석 μ‹œμž‘")
294
- print("=" * 80)
295
- print("")
296
-
297
- # 1. μƒ˜ν”Œ λ‰΄μŠ€ 생성
298
- print("πŸ“° 였늘의 AI λ‰΄μŠ€ λ‘œλ”© 쀑...")
299
- news_list = self.create_sample_news()
300
- print(f"βœ… {len(news_list)}건의 λ‰΄μŠ€ λ‘œλ“œ μ™„λ£Œ")
301
- print("")
302
-
303
- # 2. μΉ΄ν…Œκ³ λ¦¬ λΆ„λ₯˜
304
- print("🏷️ μΉ΄ν…Œκ³ λ¦¬ λΆ„λ₯˜ 쀑...")
305
- categorized = self.categorize_news(news_list)
306
- print("βœ… λΆ„λ₯˜ μ™„λ£Œ")
307
- print("")
308
-
309
- # 3. ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”©
310
- self.fetch_huggingface_trending()
311
- print("")
312
-
313
- # 4. 리포트 생성
314
- print("πŸ“ 리포트 생성 쀑...")
315
- report = self.generate_report(categorized, analyze_news=use_llm)
316
- print("βœ… 리포트 생성 μ™„λ£Œ!")
317
- print("")
318
-
319
- return report
320
-
321
-
322
- # ==================== 메인 μ‹€ν–‰ ====================
323
-
324
- def main():
325
- """메인 μ‹€ν–‰ ν•¨μˆ˜"""
326
- print("\n")
327
- print("╔════════════════════════════════════════════════════════════╗")
328
- print("β•‘ AI λ‰΄μŠ€ & ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© 뢄석 μ‹œμŠ€ν…œ v1.0 β•‘")
329
- print("β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•")
330
- print("\n")
331
-
332
- # API ν‚€ μ„€μ • (선택사항)
333
- FIREWORKS_API_KEY = None # 여기에 API ν‚€λ₯Ό μž…λ ₯ν•˜κ±°λ‚˜ None으둜 λ‘μ„Έμš”
334
- BRAVE_API_KEY = None
335
-
336
- if not FIREWORKS_API_KEY:
337
- print("ℹ️ Fireworks API ν‚€κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
338
- print(" - LLM 뢄석 κΈ°λŠ₯은 λΉ„ν™œμ„±ν™”λ©λ‹ˆλ‹€.")
339
- print(" - κΈ°λ³Έ λ‰΄μŠ€ μˆ˜μ§‘ 및 λΆ„λ₯˜λŠ” 정상 μž‘λ™ν•©λ‹ˆλ‹€.")
340
- print("")
341
-
342
- # 뢄석기 μ΄ˆκΈ°ν™”
343
- analyzer = AINewsAnalyzer(
344
- fireworks_api_key=FIREWORKS_API_KEY,
345
- brave_api_key=BRAVE_API_KEY
346
- )
347
-
348
- # μ‹€ν–‰ (use_llm=False둜 μ„€μ •ν•˜λ©΄ API 없이도 λ™μž‘)
349
- report = analyzer.run(use_llm=False)
350
-
351
- # κ²°κ³Ό 좜λ ₯
352
- print(report)
353
-
354
- # 파일 μ €μž₯
355
- timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
356
- filename = f"ai_news_report_{timestamp}.txt"
357
-
358
  try:
359
- with open(filename, 'w', encoding='utf-8') as f:
360
- f.write(report)
361
- print(f"\nπŸ’Ύ 리포트 μ €μž₯ μ™„λ£Œ: {filename}")
 
 
 
 
362
  except Exception as e:
363
- print(f"\n⚠️ 파일 μ €μž₯ μ‹€νŒ¨: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
 
365
- print("\n" + "=" * 80)
366
- print("ν”„λ‘œκ·Έλž¨ μ’…λ£Œ")
367
- print("=" * 80 + "\n")
 
 
 
368
 
 
 
 
 
 
 
369
 
370
- if __name__ == "__main__":
371
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # -*- coding: utf-8 -*-
2
  """
3
+ AI λ‰΄μŠ€ & ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© 뢄석 μ›Ή μ•± (Flask 버전) - μ™„μ „νŒ
4
+ 파일λͺ…: app.py
5
+
6
+ μ‹€ν–‰ 방법:
7
+ 1. pip install Flask requests beautifulsoup4 lxml gunicorn
8
+ 2. python app.py
9
+ 3. λΈŒλΌμš°μ €μ—μ„œ http://localhost:8080 접속
10
+
11
+ ν”„λ‘œλ•μ…˜ μ‹€ν–‰:
12
+ gunicorn -w 4 -b 0.0.0.0:8080 app:app
13
  """
14
 
15
+ from flask import Flask, render_template_string, jsonify, request
16
  import requests
17
  from bs4 import BeautifulSoup
18
  import json
19
  from datetime import datetime
20
+ from typing import List, Dict, Optional
21
+ import os
22
+ import sys
23
+
24
+ # Flask μ•± μ΄ˆκΈ°ν™”
25
+ app = Flask(__name__)
26
+ app.config['JSON_AS_ASCII'] = False # ν•œκΈ€ JSON 지원
27
 
28
+ # ============================================
29
+ # HTML ν…œν”Œλ¦Ώ (μ™„μ „νŒ)
30
+ # ============================================
31
+
32
+ HTML_TEMPLATE = """
33
+ <!DOCTYPE html>
34
+ <html lang="ko">
35
+ <head>
36
+ <meta charset="UTF-8">
37
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
38
+ <title>AI λ‰΄μŠ€ & ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© 뢄석 μ‹œμŠ€ν…œ</title>
39
+ <style>
40
+ * {
41
+ margin: 0;
42
+ padding: 0;
43
+ box-sizing: border-box;
44
+ }
45
+
46
+ body {
47
+ font-family: 'Segoe UI', 'Apple SD Gothic Neo', 'Malgun Gothic', sans-serif;
48
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
49
+ padding: 20px;
50
+ color: #333;
51
+ min-height: 100vh;
52
+ }
53
+
54
+ .container {
55
+ max-width: 1400px;
56
+ margin: 0 auto;
57
+ background: white;
58
+ border-radius: 20px;
59
+ padding: 40px;
60
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
61
+ }
62
+
63
+ h1 {
64
+ text-align: center;
65
+ color: #667eea;
66
+ margin-bottom: 10px;
67
+ font-size: 2.8em;
68
+ font-weight: 800;
69
+ }
70
+
71
+ .subtitle {
72
+ text-align: center;
73
+ color: #666;
74
+ margin-bottom: 40px;
75
+ font-size: 1.2em;
76
+ }
77
+
78
+ .stats {
79
+ display: grid;
80
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
81
+ gap: 25px;
82
+ margin-bottom: 50px;
83
+ }
84
+
85
+ .stat-card {
86
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
87
+ color: white;
88
+ padding: 30px;
89
+ border-radius: 15px;
90
+ text-align: center;
91
+ box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
92
+ transform: translateY(0);
93
+ transition: transform 0.3s, box-shadow 0.3s;
94
+ }
95
+
96
+ .stat-card:hover {
97
+ transform: translateY(-5px);
98
+ box-shadow: 0 12px 30px rgba(102, 126, 234, 0.6);
99
+ }
100
+
101
+ .stat-number {
102
+ font-size: 3.5em;
103
+ font-weight: bold;
104
+ margin-bottom: 10px;
105
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
106
+ }
107
+
108
+ .stat-label {
109
+ font-size: 1.2em;
110
+ opacity: 0.95;
111
+ font-weight: 500;
112
+ }
113
+
114
+ .category-section {
115
+ margin-bottom: 50px;
116
+ }
117
+
118
+ .category-title {
119
+ background: linear-gradient(90deg, #667eea, #764ba2);
120
+ color: white;
121
+ padding: 18px 25px;
122
+ border-radius: 12px;
123
+ font-size: 1.6em;
124
+ font-weight: 700;
125
+ margin-bottom: 25px;
126
+ display: flex;
127
+ justify-content: space-between;
128
+ align-items: center;
129
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
130
+ }
131
+
132
+ .news-item {
133
+ background: #f8f9fa;
134
+ padding: 25px;
135
+ border-radius: 12px;
136
+ margin-bottom: 20px;
137
+ border-left: 6px solid #667eea;
138
+ transition: all 0.3s;
139
+ position: relative;
140
+ }
141
+
142
+ .news-item:hover {
143
+ transform: translateX(8px);
144
+ box-shadow: 0 8px 20px rgba(0,0,0,0.12);
145
+ background: #f0f4ff;
146
+ }
147
+
148
+ .news-title {
149
+ font-size: 1.3em;
150
+ font-weight: 700;
151
+ color: #2c3e50;
152
+ margin-bottom: 12px;
153
+ line-height: 1.5;
154
+ }
155
+
156
+ .news-meta {
157
+ color: #7f8c8d;
158
+ font-size: 0.95em;
159
+ margin-bottom: 15px;
160
+ display: flex;
161
+ gap: 20px;
162
+ flex-wrap: wrap;
163
+ }
164
+
165
+ .news-link {
166
+ display: inline-block;
167
+ background: #667eea;
168
+ color: white;
169
+ padding: 10px 20px;
170
+ border-radius: 8px;
171
+ text-decoration: none;
172
+ font-size: 0.95em;
173
+ font-weight: 600;
174
+ transition: all 0.3s;
175
+ }
176
+
177
+ .news-link:hover {
178
+ background: #764ba2;
179
+ transform: scale(1.05);
180
+ box-shadow: 0 4px 10px rgba(102, 126, 234, 0.4);
181
+ }
182
+
183
+ .hf-section {
184
+ background: linear-gradient(135deg, #f0f4ff 0%, #e8f0fe 100%);
185
+ padding: 40px;
186
+ border-radius: 20px;
187
+ margin-top: 50px;
188
+ box-shadow: 0 10px 30px rgba(0,0,0,0.08);
189
+ }
190
+
191
+ .hf-title {
192
+ font-size: 2.2em;
193
+ color: #667eea;
194
+ margin-bottom: 30px;
195
+ text-align: center;
196
+ font-weight: 800;
197
+ }
198
+
199
+ .model-grid {
200
+ display: grid;
201
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
202
+ gap: 25px;
203
+ margin-top: 30px;
204
+ }
205
+
206
+ .model-card {
207
+ background: white;
208
+ padding: 25px;
209
+ border-radius: 12px;
210
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
211
+ transition: all 0.3s;
212
+ border-top: 4px solid #667eea;
213
+ }
214
+
215
+ .model-card:hover {
216
+ transform: translateY(-5px);
217
+ box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3);
218
+ }
219
+
220
+ .model-name {
221
+ font-weight: 700;
222
+ color: #667eea;
223
+ margin-bottom: 15px;
224
+ font-size: 1.15em;
225
+ word-break: break-word;
226
+ }
227
+
228
+ .model-stats {
229
+ font-size: 0.95em;
230
+ color: #555;
231
+ margin-bottom: 15px;
232
+ line-height: 1.8;
233
+ }
234
+
235
+ .model-task {
236
+ background: #e8f0fe;
237
+ color: #667eea;
238
+ padding: 6px 12px;
239
+ border-radius: 20px;
240
+ font-size: 0.85em;
241
+ display: inline-block;
242
+ margin-bottom: 15px;
243
+ font-weight: 600;
244
+ }
245
+
246
+ .button-group {
247
+ text-align: center;
248
+ margin: 40px 0;
249
+ }
250
+
251
+ .refresh-btn {
252
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
253
+ color: white;
254
+ border: none;
255
+ padding: 18px 50px;
256
+ font-size: 1.2em;
257
+ font-weight: 700;
258
+ border-radius: 50px;
259
+ cursor: pointer;
260
+ box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
261
+ transition: all 0.3s;
262
+ margin: 0 10px;
263
+ }
264
+
265
+ .refresh-btn:hover {
266
+ transform: scale(1.08);
267
+ box-shadow: 0 12px 30px rgba(102, 126, 234, 0.6);
268
+ }
269
+
270
+ .api-btn {
271
+ background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
272
+ color: white;
273
+ border: none;
274
+ padding: 18px 50px;
275
+ font-size: 1.2em;
276
+ font-weight: 700;
277
+ border-radius: 50px;
278
+ cursor: pointer;
279
+ box-shadow: 0 8px 20px rgba(17, 153, 142, 0.4);
280
+ transition: all 0.3s;
281
+ margin: 0 10px;
282
+ }
283
+
284
+ .api-btn:hover {
285
+ transform: scale(1.08);
286
+ box-shadow: 0 12px 30px rgba(17, 153, 142, 0.6);
287
+ }
288
+
289
+ .loading {
290
+ text-align: center;
291
+ padding: 60px;
292
+ font-size: 1.8em;
293
+ color: #667eea;
294
+ font-weight: 600;
295
+ }
296
+
297
+ .timestamp {
298
+ text-align: center;
299
+ color: #999;
300
+ margin-top: 40px;
301
+ font-size: 1em;
302
+ padding: 20px;
303
+ background: #f8f9fa;
304
+ border-radius: 10px;
305
+ }
306
+
307
+ .footer {
308
+ text-align: center;
309
+ margin-top: 50px;
310
+ padding-top: 30px;
311
+ border-top: 2px solid #e0e0e0;
312
+ color: #666;
313
+ }
314
+
315
+ .badge {
316
+ display: inline-block;
317
+ background: #ff6b6b;
318
+ color: white;
319
+ padding: 4px 10px;
320
+ border-radius: 12px;
321
+ font-size: 0.75em;
322
+ font-weight: 600;
323
+ margin-left: 8px;
324
+ }
325
+
326
+ @media (max-width: 768px) {
327
+ .container {
328
+ padding: 20px;
329
+ }
330
+
331
+ h1 {
332
+ font-size: 2em;
333
+ }
334
+
335
+ .stats {
336
+ grid-template-columns: repeat(2, 1fr);
337
+ }
338
+
339
+ .model-grid {
340
+ grid-template-columns: 1fr;
341
+ }
342
+
343
+ .button-group {
344
+ display: flex;
345
+ flex-direction: column;
346
+ gap: 15px;
347
+ }
348
+
349
+ .refresh-btn, .api-btn {
350
+ margin: 0;
351
+ width: 100%;
352
+ }
353
+ }
354
+
355
+ /* μ• λ‹ˆλ©”μ΄μ…˜ */
356
+ @keyframes fadeIn {
357
+ from {
358
+ opacity: 0;
359
+ transform: translateY(20px);
360
+ }
361
+ to {
362
+ opacity: 1;
363
+ transform: translateY(0);
364
+ }
365
+ }
366
+
367
+ .news-item {
368
+ animation: fadeIn 0.5s ease-out;
369
+ }
370
+
371
+ .model-card {
372
+ animation: fadeIn 0.5s ease-out;
373
+ }
374
+ </style>
375
+ </head>
376
+ <body>
377
+ <div class="container">
378
+ <h1>πŸ€– AI λ‰΄μŠ€ & ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”©</h1>
379
+ <p class="subtitle">μ‹€μ‹œκ°„ AI μ‚°μ—… 동ν–₯ 뢄석 μ‹œμŠ€ν…œ πŸ“Š</p>
380
+
381
+ <!-- 톡계 μΉ΄λ“œ -->
382
+ <div class="stats">
383
+ <div class="stat-card">
384
+ <div class="stat-number">{{ stats.total_news }}</div>
385
+ <div class="stat-label">πŸ“° 총 λ‰΄μŠ€</div>
386
+ </div>
387
+ <div class="stat-card">
388
+ <div class="stat-number">{{ stats.categories }}</div>
389
+ <div class="stat-label">πŸ“ μΉ΄ν…Œκ³ λ¦¬</div>
390
+ </div>
391
+ <div class="stat-card">
392
+ <div class="stat-number">{{ stats.hf_models }}</div>
393
+ <div class="stat-label">πŸ€— HF λͺ¨λΈ</div>
394
+ </div>
395
+ <div class="stat-card">
396
+ <div class="stat-number">{{ stats.hf_spaces }}</div>
397
+ <div class="stat-label">πŸš€ HF 슀페이슀</div>
398
+ </div>
399
+ </div>
400
+
401
+ <!-- μΉ΄ν…Œκ³ λ¦¬λ³„ λ‰΄μŠ€ -->
402
+ {% for category, articles in news_by_category.items() %}
403
+ <div class="category-section">
404
+ <div class="category-title">
405
+ <span>πŸ“Œ {{ category }}</span>
406
+ <span class="badge">{{ articles|length }}건</span>
407
+ </div>
408
+ {% for article in articles %}
409
+ <div class="news-item">
410
+ <div class="news-title">{{ loop.index }}. {{ article.title }}</div>
411
+ <div class="news-meta">
412
+ <span>πŸ“… {{ article.date }}</span>
413
+ <span>πŸ“° {{ article.source }}</span>
414
+ </div>
415
+ <a href="{{ article.url }}" target="_blank" class="news-link">
416
+ πŸ”— 기사 μ „λ¬Έ 보기
417
+ </a>
418
+ </div>
419
+ {% endfor %}
420
+ </div>
421
+ {% endfor %}
422
+
423
+ <!-- ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© λͺ¨λΈ -->
424
+ <div class="hf-section">
425
+ <div class="hf-title">πŸ€— ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© λͺ¨λΈ TOP 10</div>
426
+
427
+ {% if hf_models|length > 0 %}
428
+ <div class="model-grid">
429
+ {% for model in hf_models[:10] %}
430
+ <div class="model-card">
431
+ <div class="model-name">
432
+ {{ loop.index }}. {{ model.name }}
433
+ </div>
434
+ <div class="model-task">
435
+ 🏷️ {{ model.task }}
436
+ </div>
437
+ <div class="model-stats">
438
+ πŸ“Š λ‹€μš΄λ‘œλ“œ: <strong>{{ "{:,}".format(model.downloads) }}</strong><br>
439
+ ❀️ μ’‹μ•„μš”: <strong>{{ "{:,}".format(model.likes) }}</strong>
440
+ </div>
441
+ <a href="{{ model.url }}" target="_blank" class="news-link">
442
+ πŸ”— λͺ¨λΈ νŽ˜μ΄μ§€ λ°©λ¬Έ
443
+ </a>
444
+ </div>
445
+ {% endfor %}
446
+ </div>
447
+ {% else %}
448
+ <div class="loading">
449
+ ⚠️ λͺ¨λΈ 데이터λ₯Ό λΆˆλŸ¬μ˜€μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.
450
+ </div>
451
+ {% endif %}
452
+ </div>
453
+
454
+ <!-- λ²„νŠΌ κ·Έλ£Ή -->
455
+ <div class="button-group">
456
+ <button class="refresh-btn" onclick="location.reload()">
457
+ πŸ”„ μƒˆλ‘œκ³ μΉ¨
458
+ </button>
459
+ <button class="api-btn" onclick="window.open('/api/data', '_blank')">
460
+ πŸ“Š JSON API 보기
461
+ </button>
462
+ </div>
463
+
464
+ <!-- νƒ€μž„μŠ€νƒ¬ν”„ -->
465
+ <div class="timestamp">
466
+ ⏰ λ§ˆμ§€λ§‰ μ—…λ°μ΄νŠΈ: {{ timestamp }}
467
+ </div>
468
+
469
+ <!-- ν‘Έν„° -->
470
+ <div class="footer">
471
+ <p>πŸ€– AI λ‰΄μŠ€ 뢄석 μ‹œμŠ€ν…œ v1.0</p>
472
+ <p style="margin-top: 10px; font-size: 0.9em;">
473
+ 데이터 좜처: AI Times, Hugging Face
474
+ </p>
475
+ </div>
476
+ </div>
477
+
478
+ <script>
479
+ // μžλ™ μƒˆλ‘œκ³ μΉ¨ (5λΆ„λ§ˆλ‹€) - 선택사항
480
+ // setTimeout(() => location.reload(), 5 * 60 * 1000);
481
+
482
+ console.log('βœ… AI λ‰΄μŠ€ 뢄석 μ‹œμŠ€ν…œ λ‘œλ“œ μ™„λ£Œ');
483
+ </script>
484
+ </body>
485
+ </html>
486
+ """
487
+
488
+
489
+ # ============================================
490
+ # AINewsAnalyzer 클래슀
491
+ # ============================================
492
 
493
  class AINewsAnalyzer:
494
+ """AI λ‰΄μŠ€ 및 ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© 뢄석기"""
495
+
496
+ def __init__(self, fireworks_api_key: Optional[str] = None, brave_api_key: Optional[str] = None):
497
  """
498
+ Args:
499
+ fireworks_api_key: Fireworks AI API ν‚€ (선택)
500
+ brave_api_key: Brave Search API ν‚€ (선택)
501
  """
502
+ self.fireworks_api_key = fireworks_api_key or os.getenv('FIREWORKS_API_KEY')
503
+ self.brave_api_key = brave_api_key or os.getenv('BRAVE_API_KEY')
504
 
505
  # λ‰΄μŠ€ μΉ΄ν…Œκ³ λ¦¬ μ •μ˜
506
  self.categories = {
507
+ "산업동ν–₯": ["μ‚°μ—…", "κΈ°μ—…", "투자", "인수", "νŒŒνŠΈλ„ˆμ‹­", "μ‹œμž₯", "MS", "ꡬ글", "μ•„λ§ˆμ‘΄", "μ†Œν”„νŠΈλ±…ν¬"],
508
+ "κΈ°μˆ ν˜μ‹ ": ["기술", "λͺ¨λΈ", "μ•Œκ³ λ¦¬μ¦˜", "개발", "연ꡬ", "λ…Όλ¬Έ", "μ‚Όμ„±", "SAIT"],
509
+ "μ œν’ˆμΆœμ‹œ": ["μΆœμ‹œ", "곡개", "λ°œν‘œ", "μ„œλΉ„μŠ€", "μ œν’ˆ", "μ±—GPT", "μ†ŒλΌ", "νŒ¬μ„œ"],
510
+ "μ •μ±…κ·œμ œ": ["규제", "μ •μ±…", "법", "μ •λΆ€", "제재", "EU", "투자"],
511
  "λ³΄μ•ˆμ΄μŠˆ": ["λ³΄μ•ˆ", "취약점", "ν•΄ν‚Ή", "μœ„ν—˜", "ν”„λΌμ΄λ²„μ‹œ"],
512
  }
513
 
 
519
  self.news_data = []
520
 
521
  def fetch_huggingface_trending(self) -> Dict:
522
+ """ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© λͺ¨λΈ μˆ˜μ§‘"""
523
  print("πŸ€— ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© 정보 μˆ˜μ§‘ 쀑...")
524
 
525
  try:
 
526
  models_url = "https://huggingface.co/api/models"
527
+ params = {
528
+ 'sort': 'trending',
529
+ 'limit': 30
530
+ }
531
 
532
  response = requests.get(models_url, params=params, timeout=15)
533
 
 
550
  except Exception as e:
551
  print(f"❌ λͺ¨λΈ μˆ˜μ§‘ 였λ₯˜: {e}")
552
 
553
+ # μƒ˜ν”Œ 슀페이슀 데이터
554
  sample_spaces = [
555
  {"name": "Wan2.2-5B", "title": "κ³ ν’ˆμ§ˆ λΉ„λ””μ˜€ 생성", "url": "https://huggingface.co/spaces/"},
556
  {"name": "FLUX-Image", "title": "ν…μŠ€νŠΈβ†’μ΄λ―Έμ§€ 생성", "url": "https://huggingface.co/spaces/"},
 
558
  ]
559
 
560
  self.huggingface_data['spaces'] = sample_spaces
 
561
 
562
  return self.huggingface_data
563
 
 
653
 
654
  return news_list
655
 
656
+ def get_data(self) -> Dict:
657
+ """λͺ¨λ“  데이터 μˆ˜μ§‘ 및 λ°˜ν™˜"""
658
+ # λ‰΄μŠ€ μˆ˜μ§‘
659
+ news = self.create_sample_news()
660
+ news = self.categorize_news(news)
661
 
662
+ # ν—ˆκΉ…νŽ˜μ΄μŠ€ 데이터 μˆ˜μ§‘
663
+ hf_data = self.fetch_huggingface_trending()
664
 
665
+ # μΉ΄ν…Œκ³ λ¦¬λ³„λ‘œ λ‰΄μŠ€ κ·Έλ£Ήν™”
666
+ news_by_category = {}
667
+ for article in news:
668
+ category = article['category']
669
+ if category not in news_by_category:
670
+ news_by_category[category] = []
671
+ news_by_category[category].append(article)
 
 
672
 
673
+ # 톡계 계산
674
+ stats = {
675
+ 'total_news': len(news),
676
+ 'categories': len(news_by_category),
677
+ 'hf_models': len(hf_data['models']),
678
+ 'hf_spaces': len(hf_data['spaces'])
679
  }
680
 
681
+ return {
682
+ 'news_by_category': news_by_category,
683
+ 'hf_models': hf_data['models'],
684
+ 'hf_spaces': hf_data['spaces'],
685
+ 'stats': stats,
686
+ 'timestamp': datetime.now().strftime('%Yλ…„ %mμ›” %d일 %H:%M:%S')
687
+ }
688
+
689
+
690
+ # ============================================
691
+ # Flask 라우트 μ •μ˜
692
+ # ============================================
693
+
694
+ @app.route('/')
695
+ def index():
696
+ """메인 νŽ˜μ΄μ§€"""
697
+ try:
698
+ analyzer = AINewsAnalyzer()
699
+ data = analyzer.get_data()
700
+ return render_template_string(HTML_TEMPLATE, **data)
701
+ except Exception as e:
702
+ return f"""
703
+ <html>
704
+ <body style="font-family: Arial; padding: 50px; text-align: center;">
705
+ <h1 style="color: #e74c3c;">⚠️ 였λ₯˜ λ°œμƒ</h1>
706
+ <p>데이터λ₯Ό λΆˆλŸ¬μ˜€λŠ” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.</p>
707
+ <p style="color: #7f8c8d;">{str(e)}</p>
708
+ <button onclick="location.reload()" style="padding: 10px 20px; font-size: 16px; margin-top: 20px; cursor: pointer;">
709
+ πŸ”„ μƒˆλ‘œκ³ μΉ¨
710
+ </button>
711
+ </body>
712
+ </html>
713
+ """, 500
714
+
715
+
716
+ @app.route('/api/data')
717
+ def api_data():
718
+ """JSON API μ—”λ“œν¬μΈνŠΈ"""
719
+ try:
720
+ analyzer = AINewsAnalyzer()
721
+ data = analyzer.get_data()
722
+ return jsonify({
723
+ 'success': True,
724
+ 'data': data,
725
+ 'timestamp': datetime.now().isoformat()
726
+ })
727
+ except Exception as e:
728
+ return jsonify({
729
+ 'success': False,
730
+ 'error': str(e),
731
+ 'timestamp': datetime.now().isoformat()
732
+ }), 500
733
+
734
+
735
+ @app.route('/health')
736
+ def health():
737
+ """ν—¬μŠ€ 체크 μ—”λ“œν¬μΈνŠΈ"""
738
+ return jsonify({
739
+ "status": "healthy",
740
+ "service": "AI News Analyzer",
741
+ "version": "1.0.0",
742
+ "timestamp": datetime.now().isoformat()
743
+ })
744
+
745
+
746
+ @app.route('/api/news')
747
+ def api_news():
748
+ """λ‰΄μŠ€λ§Œ λ°˜ν™˜ν•˜λŠ” API"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
749
  try:
750
+ analyzer = AINewsAnalyzer()
751
+ news = analyzer.create_sample_news()
752
+ return jsonify({
753
+ 'success': True,
754
+ 'count': len(news),
755
+ 'news': news
756
+ })
757
  except Exception as e:
758
+ return jsonify({
759
+ 'success': False,
760
+ 'error': str(e)
761
+ }), 500
762
+
763
+
764
+ @app.route('/api/hf-models')
765
+ def api_hf_models():
766
+ """ν—ˆκΉ…νŽ˜μ΄μŠ€ λͺ¨λΈλ§Œ λ°˜ν™˜ν•˜λŠ” API"""
767
+ try:
768
+ analyzer = AINewsAnalyzer()
769
+ hf_data = analyzer.fetch_huggingface_trending()
770
+ return jsonify({
771
+ 'success': True,
772
+ 'count': len(hf_data['models']),
773
+ 'models': hf_data['models']
774
+ })
775
+ except Exception as e:
776
+ return jsonify({
777
+ 'success': False,
778
+ 'error': str(e)
779
+ }), 500
780
+
781
+
782
+ # ============================================
783
+ # 메인 μ‹€ν–‰
784
+ # ============================================
785
+
786
+ if __name__ == '__main__':
787
+ # ν™˜κ²½ λ³€μˆ˜μ—μ„œ 포트 κ°€μ Έμ˜€κΈ° (κΈ°λ³Έκ°’: 8080)
788
+ port = int(os.environ.get('PORT', 8080))
789
+
790
+ # ν™˜κ²½ λ³€μˆ˜μ—μ„œ 디버그 λͺ¨λ“œ μ„€μ • (κΈ°λ³Έκ°’: False)
791
+ debug = os.environ.get('DEBUG', 'False').lower() == 'true'
792
 
793
+ print(f"""
794
+ ╔════════════════════════════════════════════════════════════╗
795
+ β•‘ β•‘
796
+ β•‘ πŸ€– AI λ‰΄μŠ€ & ν—ˆκΉ…νŽ˜μ΄μŠ€ νŠΈλ Œλ”© μ›Ή μ•± μ‹œμž‘! β•‘
797
+ β•‘ β•‘
798
+ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
799
 
800
+ πŸš€ Flask μ„œλ²„ μ‹œμž‘ 쀑...
801
+ πŸ“ 메인 νŽ˜μ΄μ§€: http://localhost:{port}
802
+ πŸ“Š JSON API: http://localhost:{port}/api/data
803
+ πŸ“° λ‰΄μŠ€ API: http://localhost:{port}/api/news
804
+ πŸ€— λͺ¨λΈ API: http://localhost:{port}/api/hf-models
805
+ πŸ’š Health Check: http://localhost:{port}/health
806
 
807
+ {'πŸ› 디버그 λͺ¨λ“œ: ν™œμ„±ν™”' if debug else '⚑ ν”„λ‘œλ•μ…˜ λͺ¨λ“œ: μ΅œμ ν™”λ¨'}
808
+
809
+ λΈŒλΌμš°μ €μ—μ„œ μœ„ URL을 μ—΄μ–΄μ£Όμ„Έμš”!
810
+ μ’…λ£Œν•˜λ €λ©΄ Ctrl+Cλ₯Ό λˆ„λ₯΄μ„Έμš”.
811
+ """)
812
+
813
+ try:
814
+ app.run(
815
+ host='0.0.0.0',
816
+ port=port,
817
+ debug=debug,
818
+ threaded=True
819
+ )
820
+ except KeyboardInterrupt:
821
+ print("\n\nπŸ‘‹ μ„œλ²„λ₯Ό μ’…λ£Œν•©λ‹ˆλ‹€. μ•ˆλ…•νžˆ κ°€μ„Έμš”!")
822
+ sys.exit(0)
823
+ except Exception as e:
824
+ print(f"\n❌ μ„œλ²„ μ‹œμž‘ μ‹€νŒ¨: {e}")
825
+ sys.exit(1)