openfree commited on
Commit
9203871
·
verified ·
1 Parent(s): d38f8d2

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +743 -0
app.py ADDED
@@ -0,0 +1,743 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import requests
3
+ import json
4
+ import os
5
+ from datetime import datetime, timedelta
6
+ from huggingface_hub import InferenceClient
7
+
8
+ MAX_COUNTRY_RESULTS = 100 # 국가별 최대 결과 수
9
+ MAX_GLOBAL_RESULTS = 1000 # 전세계 최대 결과 수
10
+
11
+ def create_article_components(max_results):
12
+ article_components = []
13
+ for i in range(max_results):
14
+ with gr.Group(visible=False) as article_group:
15
+ title = gr.Markdown()
16
+ image = gr.Image(width=200, height=150)
17
+ snippet = gr.Markdown()
18
+ info = gr.Markdown()
19
+
20
+ article_components.append({
21
+ 'group': article_group,
22
+ 'title': title,
23
+ 'image': image,
24
+ 'snippet': snippet,
25
+ 'info': info,
26
+ 'index': i,
27
+ })
28
+ return article_components
29
+
30
+ API_KEY = os.getenv("SERPHOUSE_API_KEY")
31
+ hf_client = InferenceClient("CohereForAI/c4ai-command-r-plus-08-2024", token=os.getenv("HF_TOKEN"))
32
+
33
+ # 국가별 언어 코드 매핑
34
+ COUNTRY_LANGUAGES = {
35
+ "United States": "en",
36
+ "United Kingdom": "en",
37
+ "Taiwan": "zh-TW", # 대만어(번체 중국어)
38
+ "Canada": "en",
39
+ "Australia": "en",
40
+ "Germany": "de",
41
+ "France": "fr",
42
+ "Japan": "ja",
43
+ # "South Korea": "ko",
44
+ "China": "zh",
45
+ "India": "hi",
46
+ "Brazil": "pt",
47
+ "Mexico": "es",
48
+ "Russia": "ru",
49
+ "Italy": "it",
50
+ "Spain": "es",
51
+ "Netherlands": "nl",
52
+ "Singapore": "en",
53
+ "Hong Kong": "zh-HK",
54
+ "Indonesia": "id",
55
+ "Malaysia": "ms",
56
+ "Philippines": "tl",
57
+ "Thailand": "th",
58
+ "Vietnam": "vi",
59
+ "Belgium": "nl",
60
+ "Denmark": "da",
61
+ "Finland": "fi",
62
+ "Ireland": "en",
63
+ "Norway": "no",
64
+ "Poland": "pl",
65
+ "Sweden": "sv",
66
+ "Switzerland": "de",
67
+ "Austria": "de",
68
+ "Czech Republic": "cs",
69
+ "Greece": "el",
70
+ "Hungary": "hu",
71
+ "Portugal": "pt",
72
+ "Romania": "ro",
73
+ "Turkey": "tr",
74
+ "Israel": "he",
75
+ "Saudi Arabia": "ar",
76
+ "United Arab Emirates": "ar",
77
+ "South Africa": "en",
78
+ "Argentina": "es",
79
+ "Chile": "es",
80
+ "Colombia": "es",
81
+ "Peru": "es",
82
+ "Venezuela": "es",
83
+ "New Zealand": "en",
84
+ "Bangladesh": "bn",
85
+ "Pakistan": "ur",
86
+ "Egypt": "ar",
87
+ "Morocco": "ar",
88
+ "Nigeria": "en",
89
+ "Kenya": "sw",
90
+ "Ukraine": "uk",
91
+ "Croatia": "hr",
92
+ "Slovakia": "sk",
93
+ "Bulgaria": "bg",
94
+ "Serbia": "sr",
95
+ "Estonia": "et",
96
+ "Latvia": "lv",
97
+ "Lithuania": "lt",
98
+ "Slovenia": "sl",
99
+ "Luxembourg": "fr",
100
+ "Malta": "mt",
101
+ "Cyprus": "el",
102
+ "Iceland": "is"
103
+ }
104
+
105
+ COUNTRY_LOCATIONS = {
106
+ "United States": "United States",
107
+ "United Kingdom": "United Kingdom",
108
+ "Taiwan": "Taiwan", # 국가명 사용
109
+ "Canada": "Canada",
110
+ "Australia": "Australia",
111
+ "Germany": "Germany",
112
+ "France": "France",
113
+ "Japan": "Japan",
114
+ # "South Korea": "South Korea",
115
+ "China": "China",
116
+ "India": "India",
117
+ "Brazil": "Brazil",
118
+ "Mexico": "Mexico",
119
+ "Russia": "Russia",
120
+ "Italy": "Italy",
121
+ "Spain": "Spain",
122
+ "Netherlands": "Netherlands",
123
+ "Singapore": "Singapore",
124
+ "Hong Kong": "Hong Kong",
125
+ "Indonesia": "Indonesia",
126
+ "Malaysia": "Malaysia",
127
+ "Philippines": "Philippines",
128
+ "Thailand": "Thailand",
129
+ "Vietnam": "Vietnam",
130
+ "Belgium": "Belgium",
131
+ "Denmark": "Denmark",
132
+ "Finland": "Finland",
133
+ "Ireland": "Ireland",
134
+ "Norway": "Norway",
135
+ "Poland": "Poland",
136
+ "Sweden": "Sweden",
137
+ "Switzerland": "Switzerland",
138
+ "Austria": "Austria",
139
+ "Czech Republic": "Czech Republic",
140
+ "Greece": "Greece",
141
+ "Hungary": "Hungary",
142
+ "Portugal": "Portugal",
143
+ "Romania": "Romania",
144
+ "Turkey": "Turkey",
145
+ "Israel": "Israel",
146
+ "Saudi Arabia": "Saudi Arabia",
147
+ "United Arab Emirates": "United Arab Emirates",
148
+ "South Africa": "South Africa",
149
+ "Argentina": "Argentina",
150
+ "Chile": "Chile",
151
+ "Colombia": "Colombia",
152
+ "Peru": "Peru",
153
+ "Venezuela": "Venezuela",
154
+ "New Zealand": "New Zealand",
155
+ "Bangladesh": "Bangladesh",
156
+ "Pakistan": "Pakistan",
157
+ "Egypt": "Egypt",
158
+ "Morocco": "Morocco",
159
+ "Nigeria": "Nigeria",
160
+ "Kenya": "Kenya",
161
+ "Ukraine": "Ukraine",
162
+ "Croatia": "Croatia",
163
+ "Slovakia": "Slovakia",
164
+ "Bulgaria": "Bulgaria",
165
+ "Serbia": "Serbia",
166
+ "Estonia": "Estonia",
167
+ "Latvia": "Latvia",
168
+ "Lithuania": "Lithuania",
169
+ "Slovenia": "Slovenia",
170
+ "Luxembourg": "Luxembourg",
171
+ "Malta": "Malta",
172
+ "Cyprus": "Cyprus",
173
+ "Iceland": "Iceland"
174
+ }
175
+
176
+ MAJOR_COUNTRIES = list(COUNTRY_LOCATIONS.keys())
177
+
178
+ def translate_query(query, country):
179
+ try:
180
+ # 영어 입력 확인
181
+ if is_english(query):
182
+ print(f"영어 검색어 감지 - 원본 사용: {query}")
183
+ return query
184
+
185
+ # 선택된 국가가 번역 지원 국가인 경우
186
+ if country in COUNTRY_LANGUAGES:
187
+ # South Korea 선택시 한글 입력은 그대로 사용
188
+ if country == "South Korea":
189
+ print(f"한국 선택 - 원본 사용: {query}")
190
+ return query
191
+
192
+ target_lang = COUNTRY_LANGUAGES[country]
193
+ print(f"번역 시도: {query} -> {country}({target_lang})")
194
+
195
+ url = f"https://translate.googleapis.com/translate_a/single"
196
+ params = {
197
+ "client": "gtx",
198
+ "sl": "auto",
199
+ "tl": target_lang,
200
+ "dt": "t",
201
+ "q": query
202
+ }
203
+
204
+ response = requests.get(url, params=params)
205
+ translated_text = response.json()[0][0][0]
206
+ print(f"번역 완료: {query} -> {translated_text} ({country})")
207
+ return translated_text
208
+
209
+ return query
210
+
211
+ except Exception as e:
212
+ print(f"번역 오류: {str(e)}")
213
+ return query
214
+
215
+ def translate_to_korean(text):
216
+ try:
217
+ url = "https://translate.googleapis.com/translate_a/single"
218
+ params = {
219
+ "client": "gtx",
220
+ "sl": "auto",
221
+ "tl": "ko",
222
+ "dt": "t",
223
+ "q": text
224
+ }
225
+
226
+ response = requests.get(url, params=params)
227
+ translated_text = response.json()[0][0][0]
228
+ return translated_text
229
+ except Exception as e:
230
+ print(f"한글 번역 오류: {str(e)}")
231
+ return text
232
+
233
+ def is_english(text):
234
+ return all(ord(char) < 128 for char in text.replace(' ', '').replace('-', '').replace('_', ''))
235
+
236
+ def is_korean(text):
237
+ return any('\uAC00' <= char <= '\uD7A3' for char in text)
238
+
239
+ def search_serphouse(query, country, page=1, num_result=10):
240
+ url = "https://api.serphouse.com/serp/live"
241
+
242
+ now = datetime.utcnow()
243
+ yesterday = now - timedelta(days=1)
244
+ date_range = f"{yesterday.strftime('%Y-%m-%d')},{now.strftime('%Y-%m-%d')}"
245
+
246
+ translated_query = translate_query(query, country)
247
+ print(f"Original query: {query}")
248
+ print(f"Translated query: {translated_query}")
249
+
250
+ payload = {
251
+ "data": {
252
+ "q": translated_query,
253
+ "domain": "google.com",
254
+ "loc": COUNTRY_LOCATIONS.get(country, "United States"),
255
+ "lang": COUNTRY_LANGUAGES.get(country, "en"),
256
+ "device": "desktop",
257
+ "serp_type": "news",
258
+ "page": "1",
259
+ "num": "10",
260
+ "date_range": date_range,
261
+ "sort_by": "date"
262
+ }
263
+ }
264
+
265
+ headers = {
266
+ "accept": "application/json",
267
+ "content-type": "application/json",
268
+ "authorization": f"Bearer {API_KEY}"
269
+ }
270
+
271
+ try:
272
+ response = requests.post(url, json=payload, headers=headers)
273
+ print("Request payload:", json.dumps(payload, indent=2, ensure_ascii=False))
274
+ print("Response status:", response.status_code)
275
+
276
+ response.raise_for_status()
277
+ return {"results": response.json(), "translated_query": translated_query}
278
+ except requests.RequestException as e:
279
+ return {"error": f"Error: {str(e)}", "translated_query": query}
280
+
281
+ def format_results_from_raw(response_data):
282
+ if "error" in response_data:
283
+ return "Error: " + response_data["error"], []
284
+
285
+ try:
286
+ results = response_data["results"]
287
+ translated_query = response_data["translated_query"]
288
+
289
+ news_results = results.get('results', {}).get('results', {}).get('news', [])
290
+ if not news_results:
291
+ return "검색 결과가 없습니다.", []
292
+
293
+ articles = []
294
+ for idx, result in enumerate(news_results, 1):
295
+ articles.append({
296
+ "index": idx,
297
+ "title": result.get("title", "제목 없음"),
298
+ "link": result.get("url", result.get("link", "#")),
299
+ "snippet": result.get("snippet", "내용 없음"),
300
+ "channel": result.get("channel", result.get("source", "알 수 없음")),
301
+ "time": result.get("time", result.get("date", "알 수 없는 시간")),
302
+ "image_url": result.get("img", result.get("thumbnail", "")),
303
+ "translated_query": translated_query
304
+ })
305
+ return "", articles
306
+ except Exception as e:
307
+ return f"결과 처리 중 오류 발생: {str(e)}", []
308
+
309
+ def serphouse_search(query, country):
310
+ response_data = search_serphouse(query, country)
311
+ return format_results_from_raw(response_data)
312
+
313
+
314
+ # Hacker News API 관련 함수들 먼저 추가
315
+ def get_hn_item(item_id):
316
+ """개별 아이템 정보 가져오기"""
317
+ try:
318
+ response = requests.get(f"https://hacker-news.firebaseio.com/v0/item/{item_id}.json")
319
+ return response.json()
320
+ except:
321
+ return None
322
+
323
+ def get_recent_stories():
324
+ """최신 스토리 가져오기"""
325
+ try:
326
+ response = requests.get("https://hacker-news.firebaseio.com/v0/newstories.json")
327
+ story_ids = response.json()
328
+
329
+ recent_stories = []
330
+ current_time = datetime.now().timestamp()
331
+ day_ago = current_time - (24 * 60 * 60)
332
+
333
+ for story_id in story_ids:
334
+ story = get_hn_item(story_id)
335
+ if story and 'time' in story and story['time'] > day_ago:
336
+ recent_stories.append(story)
337
+
338
+ if len(recent_stories) >= 100:
339
+ break
340
+
341
+ return recent_stories
342
+ except Exception as e:
343
+ print(f"Error fetching HN stories: {str(e)}")
344
+ return []
345
+
346
+ def format_hn_time(timestamp):
347
+ """Unix timestamp를 읽기 쉬운 형식으로 변환"""
348
+ try:
349
+ dt = datetime.fromtimestamp(timestamp)
350
+ return dt.strftime("%Y-%m-%d %H:%M:%S")
351
+ except:
352
+ return "Unknown time"
353
+
354
+ def refresh_hn_stories():
355
+ """Hacker News 스토리 새로고침"""
356
+ status_msg = "Hacker News 포스트를 가져오는 중..."
357
+
358
+ outputs = [gr.update(value=status_msg, visible=True)]
359
+
360
+ # 모든 컴포넌트 초기화
361
+ for _ in hn_article_components:
362
+ outputs.extend([
363
+ gr.update(visible=False),
364
+ gr.update(),
365
+ gr.update()
366
+ ])
367
+
368
+ # 최신 스토리 가져오기
369
+ stories = get_recent_stories()
370
+
371
+ # 결과 업데이트
372
+ outputs = [gr.update(value=f"총 {len(stories)}개의 포스트를 찾았습니다.", visible=True)]
373
+
374
+ for idx, comp in enumerate(hn_article_components):
375
+ if idx < len(stories):
376
+ story = stories[idx]
377
+ outputs.extend([
378
+ gr.update(visible=True),
379
+ gr.update(value=f"### [{story.get('title', 'Untitled')}]({story.get('url', '#')})"),
380
+ gr.update(value=f"**작성자:** {story.get('by', 'unknown')} | **시간:** {format_hn_time(story.get('time', 0))} | **점수:** {story.get('score', 0)} | **댓글:** {len(story.get('kids', []))}개")
381
+ ])
382
+ else:
383
+ outputs.extend([
384
+ gr.update(visible=False),
385
+ gr.update(),
386
+ gr.update()
387
+ ])
388
+
389
+ return outputs
390
+
391
+
392
+ css = """
393
+ footer {visibility: hidden;}
394
+ #status_area {
395
+ background: rgba(255, 255, 255, 0.9); /* 약간 투명한 흰색 배경 */
396
+ padding: 15px;
397
+ border-bottom: 1px solid #ddd;
398
+ margin-bottom: 20px;
399
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1); /* 부드러운 그림자 효과 */
400
+ }
401
+ #results_area {
402
+ padding: 10px;
403
+ margin-top: 10px;
404
+ }
405
+ /* 탭 스타일 개선 */
406
+ .tabs {
407
+ border-bottom: 2px solid #ddd !important;
408
+ margin-bottom: 20px !important;
409
+ }
410
+ .tab-nav {
411
+ border-bottom: none !important;
412
+ margin-bottom: 0 !important;
413
+ }
414
+ .tab-nav button {
415
+ font-weight: bold !important;
416
+ padding: 10px 20px !important;
417
+ }
418
+ .tab-nav button.selected {
419
+ border-bottom: 2px solid #1f77b4 !important; /* 선택된 탭 강조 */
420
+ color: #1f77b4 !important;
421
+ }
422
+ /* 검색 상태 메시지 스타일 */
423
+ #status_area .markdown-text {
424
+ font-size: 1.1em;
425
+ color: #2c3e50;
426
+ padding: 10px 0;
427
+ }
428
+ /* 검색 결과 컨테이너 스타일 */
429
+ .group {
430
+ border: 1px solid #eee;
431
+ padding: 15px;
432
+ margin-bottom: 15px;
433
+ border-radius: 5px;
434
+ background: white;
435
+ }
436
+ /* 검색 버튼 스타일 */
437
+ .primary-btn {
438
+ background: #1f77b4 !important;
439
+ border: none !important;
440
+ }
441
+ /* 검색어 입력창 스타일 */
442
+ .textbox {
443
+ border: 1px solid #ddd !important;
444
+ border-radius: 4px !important;
445
+ }
446
+ """
447
+
448
+ with gr.Blocks(theme="Nymbo/Nymbo_Theme", css=css, title="NewsAI 서비스") as iface:
449
+ with gr.Tabs():
450
+ # 국가별 탭
451
+ with gr.Tab("국가별"):
452
+ gr.Markdown("검색어를 입력하고 원하는 국가(한국 제외)를를 선택하면, 검색어와 일치하는 24시간 이내 뉴스를 최대 100개 출력합니다.")
453
+ gr.Markdown("국가 선택후 검색어에 '한글'을 입력하면 현지 언어로 번역되어 검색합니다. 예: 'Taiwan' 국가 선택후 '삼성' 입력시 '三星'으로 자동 검색")
454
+
455
+ with gr.Column():
456
+ with gr.Row():
457
+ query = gr.Textbox(label="검색어")
458
+ country = gr.Dropdown(MAJOR_COUNTRIES, label="국가", value="South Korea")
459
+
460
+ status_message = gr.Markdown("", visible=True)
461
+ translated_query_display = gr.Markdown(visible=False)
462
+ search_button = gr.Button("검색", variant="primary")
463
+
464
+ progress = gr.Progress()
465
+ articles_state = gr.State([])
466
+
467
+ article_components = []
468
+ for i in range(100):
469
+ with gr.Group(visible=False) as article_group:
470
+ title = gr.Markdown()
471
+ image = gr.Image(width=200, height=150)
472
+ snippet = gr.Markdown()
473
+ info = gr.Markdown()
474
+
475
+ article_components.append({
476
+ 'group': article_group,
477
+ 'title': title,
478
+ 'image': image,
479
+ 'snippet': snippet,
480
+ 'info': info,
481
+ 'index': i,
482
+ })
483
+
484
+ # 전세계 탭
485
+ with gr.Tab("전세계"):
486
+ gr.Markdown("검색어를 입력하면 67개국(한국 제외) 전체에 대해 국가별로 구분하여 24시간 이내 뉴스가 최대 1000개 순차 출력됩니다.")
487
+ gr.Markdown("국가 선택후 검색어에 '한글'을 입력하면 현지 언어로 번역되어 검색합니다. 예: 'Taiwan' 국가 선택후 '삼성' 입력시 '三星'으로 자동 검색")
488
+
489
+ with gr.Column():
490
+ with gr.Column(elem_id="status_area"):
491
+ with gr.Row():
492
+ query_global = gr.Textbox(label="검색어")
493
+ search_button_global = gr.Button("전세계 검색", variant="primary")
494
+
495
+ status_message_global = gr.Markdown("")
496
+ translated_query_display_global = gr.Markdown("")
497
+
498
+ with gr.Column(elem_id="results_area"):
499
+ articles_state_global = gr.State([])
500
+
501
+ global_article_components = []
502
+ for i in range(1000):
503
+ with gr.Group(visible=False) as article_group:
504
+ title = gr.Markdown()
505
+ image = gr.Image(width=200, height=150)
506
+ snippet = gr.Markdown()
507
+ info = gr.Markdown()
508
+
509
+ global_article_components.append({
510
+ 'group': article_group,
511
+ 'title': title,
512
+ 'image': image,
513
+ 'snippet': snippet,
514
+ 'info': info,
515
+ 'index': i,
516
+ })
517
+
518
+ # AI 리포터 탭
519
+ with gr.Tab("AI 리포터"):
520
+ gr.Markdown("지난 24시간 동안의 Hacker News 포스트를 보여줍니다.")
521
+
522
+ with gr.Column():
523
+ refresh_button = gr.Button("새로고침", variant="primary")
524
+ status_message_hn = gr.Markdown("")
525
+
526
+ with gr.Column(elem_id="hn_results_area"):
527
+ hn_articles_state = gr.State([])
528
+
529
+ hn_article_components = []
530
+ for i in range(100):
531
+ with gr.Group(visible=False) as article_group:
532
+ title = gr.Markdown()
533
+ info = gr.Markdown()
534
+
535
+ hn_article_components.append({
536
+ 'group': article_group,
537
+ 'title': title,
538
+ 'info': info,
539
+ 'index': i,
540
+ })
541
+
542
+
543
+
544
+
545
+
546
+
547
+
548
+ # 기존 함수들
549
+ def search_and_display(query, country, articles_state, progress=gr.Progress()):
550
+ status_msg = "검색을 진행중입니다. 잠시만 기다리세요..."
551
+
552
+ progress(0, desc="검색어 번역 중...")
553
+ translated_query = translate_query(query, country)
554
+ translated_display = f"**원본 검색어:** {query}\n**번역된 검색어:** {translated_query}" if translated_query != query else f"**검색어:** {query}"
555
+
556
+ progress(0.2, desc="검색 시작...")
557
+ error_message, articles = serphouse_search(query, country)
558
+ progress(0.5, desc="결과 처리 중...")
559
+
560
+ outputs = []
561
+ outputs.append(gr.update(value=status_msg, visible=True))
562
+ outputs.append(gr.update(value=translated_display, visible=True))
563
+
564
+ if error_message:
565
+ outputs.append(gr.update(value=error_message, visible=True))
566
+ for comp in article_components:
567
+ outputs.extend([
568
+ gr.update(visible=False), gr.update(), gr.update(),
569
+ gr.update(), gr.update()
570
+ ])
571
+ articles_state = []
572
+ else:
573
+ outputs.append(gr.update(value="", visible=False))
574
+ total_articles = len(articles)
575
+ for idx, comp in enumerate(article_components):
576
+ progress((idx + 1) / total_articles, desc=f"결과 표시 중... {idx + 1}/{total_articles}")
577
+ if idx < len(articles):
578
+ article = articles[idx]
579
+ image_url = article['image_url']
580
+ image_update = gr.update(value=image_url, visible=True) if image_url and not image_url.startswith('data:image') else gr.update(value=None, visible=False)
581
+
582
+ korean_summary = translate_to_korean(article['snippet'])
583
+
584
+ outputs.extend([
585
+ gr.update(visible=True),
586
+ gr.update(value=f"### [{article['title']}]({article['link']})"),
587
+ image_update,
588
+ gr.update(value=f"**요약:** {article['snippet']}\n\n**한글 요약:** {korean_summary}"),
589
+ gr.update(value=f"**출처:** {article['channel']} | **시간:** {article['time']}")
590
+ ])
591
+ else:
592
+ outputs.extend([
593
+ gr.update(visible=False), gr.update(), gr.update(),
594
+ gr.update(), gr.update()
595
+ ])
596
+ articles_state = articles
597
+
598
+ progress(1.0, desc="완료!")
599
+ outputs.append(articles_state)
600
+ outputs[0] = gr.update(value="", visible=False)
601
+
602
+ return outputs
603
+
604
+ def search_global(query, articles_state_global):
605
+ status_msg = "전세계 검색을 시작합니다..."
606
+ all_results = []
607
+
608
+ outputs = [
609
+ gr.update(value=status_msg, visible=True),
610
+ gr.update(value=f"**검색어:** {query}", visible=True),
611
+ ]
612
+
613
+ for _ in global_article_components:
614
+ outputs.extend([
615
+ gr.update(visible=False), gr.update(), gr.update(),
616
+ gr.update(), gr.update()
617
+ ])
618
+ outputs.append([])
619
+
620
+ yield outputs
621
+
622
+ total_countries = len(COUNTRY_LOCATIONS)
623
+ for idx, (country, location) in enumerate(COUNTRY_LOCATIONS.items(), 1):
624
+ try:
625
+ status_msg = f"{country} 검색 중... ({idx}/{total_countries} 국가)"
626
+ outputs[0] = gr.update(value=status_msg, visible=True)
627
+ yield outputs
628
+
629
+ error_message, articles = serphouse_search(query, country)
630
+ if not error_message and articles:
631
+ for article in articles:
632
+ article['source_country'] = country
633
+
634
+ all_results.extend(articles)
635
+ sorted_results = sorted(all_results, key=lambda x: x.get('time', ''), reverse=True)
636
+
637
+ seen_urls = set()
638
+ unique_results = []
639
+ for article in sorted_results:
640
+ url = article.get('link', '')
641
+ if url not in seen_urls:
642
+ seen_urls.add(url)
643
+ unique_results.append(article)
644
+
645
+ unique_results = unique_results[:1000]
646
+
647
+ outputs = [
648
+ gr.update(value=f"{idx}/{total_countries} 국가 검색 완료\n현재까지 발견된 뉴스: {len(unique_results)}건", visible=True),
649
+ gr.update(value=f"**검색어:** {query}", visible=True),
650
+ ]
651
+
652
+ for idx, comp in enumerate(global_article_components):
653
+ if idx < len(unique_results):
654
+ article = unique_results[idx]
655
+ image_url = article.get('image_url', '')
656
+ image_update = gr.update(value=image_url, visible=True) if image_url and not image_url.startswith('data:image') else gr.update(value=None, visible=False)
657
+
658
+ korean_summary = translate_to_korean(article['snippet'])
659
+
660
+ outputs.extend([
661
+ gr.update(visible=True),
662
+ gr.update(value=f"### [{article['title']}]({article['link']})"),
663
+ image_update,
664
+ gr.update(value=f"**요약:** {article['snippet']}\n\n**한글 요약:** {korean_summary}"),
665
+ gr.update(value=f"**출처:** {article['channel']} | **국가:** {article['source_country']} | **시간:** {article['time']}")
666
+ ])
667
+ else:
668
+ outputs.extend([
669
+ gr.update(visible=False), gr.update(), gr.update(),
670
+ gr.update(), gr.update()
671
+ ])
672
+
673
+ outputs.append(unique_results)
674
+ yield outputs
675
+
676
+ except Exception as e:
677
+ print(f"Error searching {country}: {str(e)}")
678
+ continue
679
+
680
+ final_status = f"검색 완료! 총 {len(unique_results)}개의 뉴스가 발견되었습니다."
681
+ outputs[0] = gr.update(value=final_status, visible=True)
682
+ yield outputs
683
+
684
+
685
+
686
+
687
+
688
+
689
+ # 국가별 탭 이벤트 연결
690
+ search_outputs = [
691
+ status_message,
692
+ translated_query_display,
693
+ gr.Markdown(visible=False)
694
+ ]
695
+
696
+ for comp in article_components:
697
+ search_outputs.extend([
698
+ comp['group'], comp['title'], comp['image'],
699
+ comp['snippet'], comp['info']
700
+ ])
701
+ search_outputs.append(articles_state)
702
+
703
+ search_button.click(
704
+ search_and_display,
705
+ inputs=[query, country, articles_state],
706
+ outputs=search_outputs,
707
+ show_progress=True
708
+ )
709
+
710
+ # 전세계 탭 이벤트 연결
711
+ global_search_outputs = [
712
+ status_message_global,
713
+ translated_query_display_global,
714
+ ]
715
+
716
+ for comp in global_article_components:
717
+ global_search_outputs.extend([
718
+ comp['group'], comp['title'], comp['image'],
719
+ comp['snippet'], comp['info']
720
+ ])
721
+ global_search_outputs.append(articles_state_global)
722
+
723
+ search_button_global.click(
724
+ search_global,
725
+ inputs=[query_global, articles_state_global],
726
+ outputs=global_search_outputs
727
+ )
728
+
729
+ # AI 리포터 탭 이벤트 연결
730
+ hn_outputs = [status_message_hn]
731
+ for comp in hn_article_components:
732
+ hn_outputs.extend([
733
+ comp['group'],
734
+ comp['title'],
735
+ comp['info']
736
+ ])
737
+
738
+ refresh_button.click(
739
+ refresh_hn_stories,
740
+ outputs=hn_outputs
741
+ )
742
+
743
+ iface.launch(auth=("it1","chosun1"))