ginipick commited on
Commit
5a8f642
·
verified ·
1 Parent(s): 66efbc8

Update app-backup2.py

Browse files
Files changed (1) hide show
  1. app-backup2.py +272 -872
app-backup2.py CHANGED
@@ -9,12 +9,83 @@ from requests.adapters import HTTPAdapter
9
  from requests.packages.urllib3.util.retry import Retry
10
  from openai import OpenAI
11
  from bs4 import BeautifulSoup
12
- import re # re 모듈 추가
13
- import json
14
- import os
15
- from datetime import datetime
16
- import sqlite3
17
  import pathlib
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
  # DB 초기화 함수
20
  def init_db():
@@ -30,16 +101,27 @@ def init_db():
30
  conn.commit()
31
  conn.close()
32
 
33
- # 검색 결과 저장 함수
34
  def save_to_db(keyword, country, results):
35
  conn = sqlite3.connect("search_results.db")
36
  c = conn.cursor()
37
- c.execute("INSERT INTO searches (keyword, country, results) VALUES (?, ?, ?)",
38
- (keyword, country, json.dumps(results)))
 
 
 
 
 
 
 
 
 
 
 
39
  conn.commit()
40
  conn.close()
41
 
42
- # DB에서 검색 결과 불러오기 함수
43
  def load_from_db(keyword, country):
44
  conn = sqlite3.connect("search_results.db")
45
  c = conn.cursor()
@@ -48,25 +130,10 @@ def load_from_db(keyword, country):
48
  result = c.fetchone()
49
  conn.close()
50
  if result:
51
- return json.loads(result[0]), result[1]
52
  return None, None
53
 
54
- # 삼성/미국 검색 함수
55
- def search_samsung_us():
56
- error_message, articles = serphouse_search("samsung", "United States")
57
- if not error_message and articles:
58
- save_to_db("samsung", "United States", articles)
59
- return display_results(articles)
60
- return "검색 결과가 없습니다."
61
 
62
- # DB에서 삼성/미국 결과 불러오기 함수
63
- def load_samsung_us():
64
- results, timestamp = load_from_db("samsung", "United States")
65
- if results:
66
- return f"저장 시간: {timestamp}\n\n" + display_results(results)
67
- return "저장된 결과가 없습니다."
68
-
69
- # 결과 표시 함수
70
  def display_results(articles):
71
  output = ""
72
  for idx, article in enumerate(articles, 1):
@@ -77,7 +144,109 @@ def display_results(articles):
77
  output += f"요약: {article['snippet']}\n\n"
78
  return output
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  ACCESS_TOKEN = os.getenv("HF_TOKEN")
82
  if not ACCESS_TOKEN:
83
  raise ValueError("HF_TOKEN environment variable is not set")
@@ -87,31 +256,10 @@ client = OpenAI(
87
  api_key=ACCESS_TOKEN,
88
  )
89
 
90
- MAX_COUNTRY_RESULTS = 100 # 국가별 최대 결과 수
91
- MAX_GLOBAL_RESULTS = 1000 # 전세계 최대 결과 수
92
-
93
- def create_article_components(max_results):
94
- article_components = []
95
- for i in range(max_results):
96
- with gr.Group(visible=False) as article_group:
97
- title = gr.Markdown()
98
- image = gr.Image(width=200, height=150)
99
- snippet = gr.Markdown()
100
- info = gr.Markdown()
101
-
102
- article_components.append({
103
- 'group': article_group,
104
- 'title': title,
105
- 'image': image,
106
- 'snippet': snippet,
107
- 'info': info,
108
- 'index': i,
109
- })
110
- return article_components
111
-
112
  API_KEY = os.getenv("SERPHOUSE_API_KEY")
113
 
114
- # 국가별 언어 코드 매핑
 
115
  COUNTRY_LANGUAGES = {
116
  "United States": "en",
117
  "KOREA": "ko",
@@ -244,193 +392,16 @@ COUNTRY_LOCATIONS = {
244
  "Slovakia": "Slovakia",
245
  "Bulgaria": "Bulgaria",
246
  "Serbia": "Serbia",
247
- "Estonia": "Estonia",
248
- "Latvia": "Latvia",
249
- "Lithuania": "Lithuania",
250
- "Slovenia": "Slovenia",
251
- "Luxembourg": "Luxembourg",
252
- "Malta": "Malta",
253
- "Cyprus": "Cyprus",
254
- "Iceland": "Iceland"
255
- }
256
-
257
- # 지역 정의
258
- # 동아시아 지역
259
- COUNTRY_LANGUAGES_EAST_ASIA = {
260
- "KOREA": "ko",
261
- "Taiwan": "zh-TW",
262
- "Japan": "ja",
263
- "China": "zh",
264
- "Hong Kong": "zh-HK"
265
- }
266
-
267
- COUNTRY_LOCATIONS_EAST_ASIA = {
268
- "KOREA": "KOREA",
269
- "Taiwan": "Taiwan",
270
- "Japan": "Japan",
271
- "China": "China",
272
- "Hong Kong": "Hong Kong"
273
- }
274
-
275
- # 동남아시아/오세아니아 지역
276
- COUNTRY_LANGUAGES_SOUTHEAST_ASIA_OCEANIA = {
277
- "Indonesia": "id",
278
- "Malaysia": "ms",
279
- "Philippines": "tl",
280
- "Thailand": "th",
281
- "Vietnam": "vi",
282
- "Singapore": "en",
283
- "Papua New Guinea": "en",
284
- "Australia": "en",
285
- "New Zealand": "en"
286
- }
287
-
288
- COUNTRY_LOCATIONS_SOUTHEAST_ASIA_OCEANIA = {
289
- "Indonesia": "Indonesia",
290
- "Malaysia": "Malaysia",
291
- "Philippines": "Philippines",
292
- "Thailand": "Thailand",
293
- "Vietnam": "Vietnam",
294
- "Singapore": "Singapore",
295
- "Papua New Guinea": "Papua New Guinea",
296
- "Australia": "Australia",
297
- "New Zealand": "New Zealand"
298
- }
299
-
300
- # 동유럽 지역
301
- COUNTRY_LANGUAGES_EAST_EUROPE = {
302
- "Poland": "pl",
303
- "Czech Republic": "cs",
304
- "Greece": "el",
305
- "Hungary": "hu",
306
- "Romania": "ro",
307
- "Ukraine": "uk",
308
- "Croatia": "hr",
309
- "Slovakia": "sk",
310
- "Bulgaria": "bg",
311
- "Serbia": "sr",
312
  "Estonia": "et",
313
  "Latvia": "lv",
314
  "Lithuania": "lt",
315
  "Slovenia": "sl",
316
- "Malta": "mt",
317
- "Cyprus": "el",
318
- "Iceland": "is",
319
- "Russia": "ru"
320
- }
321
-
322
- COUNTRY_LOCATIONS_EAST_EUROPE = {
323
- "Poland": "Poland",
324
- "Czech Republic": "Czech Republic",
325
- "Greece": "Greece",
326
- "Hungary": "Hungary",
327
- "Romania": "Romania",
328
- "Ukraine": "Ukraine",
329
- "Croatia": "Croatia",
330
- "Slovakia": "Slovakia",
331
- "Bulgaria": "Bulgaria",
332
- "Serbia": "Serbia",
333
- "Estonia": "Estonia",
334
- "Latvia": "Latvia",
335
- "Lithuania": "Lithuania",
336
- "Slovenia": "Slovenia",
337
  "Malta": "Malta",
338
  "Cyprus": "Cyprus",
339
- "Iceland": "Iceland",
340
- "Russia": "Russia"
341
- }
342
-
343
- # 서유럽 지역
344
- COUNTRY_LANGUAGES_WEST_EUROPE = {
345
- "Germany": "de",
346
- "France": "fr",
347
- "Italy": "it",
348
- "Spain": "es",
349
- "Netherlands": "nl",
350
- "Belgium": "nl",
351
- "Ireland": "en",
352
- "Sweden": "sv",
353
- "Switzerland": "de",
354
- "Austria": "de",
355
- "Portugal": "pt",
356
- "Luxembourg": "fr",
357
- "United Kingdom": "en"
358
- }
359
-
360
- COUNTRY_LOCATIONS_WEST_EUROPE = {
361
- "Germany": "Germany",
362
- "France": "France",
363
- "Italy": "Italy",
364
- "Spain": "Spain",
365
- "Netherlands": "Netherlands",
366
- "Belgium": "Belgium",
367
- "Ireland": "Ireland",
368
- "Sweden": "Sweden",
369
- "Switzerland": "Switzerland",
370
- "Austria": "Austria",
371
- "Portugal": "Portugal",
372
- "Luxembourg": "Luxembourg",
373
- "United Kingdom": "United Kingdom"
374
- }
375
-
376
- # 중동/아프리카 지역
377
- COUNTRY_LANGUAGES_ARAB_AFRICA = {
378
- "South Africa": "en",
379
- "Nigeria": "en",
380
- "Kenya": "sw",
381
- "Egypt": "ar",
382
- "Morocco": "ar",
383
- "Saudi Arabia": "ar",
384
- "United Arab Emirates": "ar",
385
- "Israel": "he"
386
- }
387
-
388
- COUNTRY_LOCATIONS_ARAB_AFRICA = {
389
- "South Africa": "South Africa",
390
- "Nigeria": "Nigeria",
391
- "Kenya": "Kenya",
392
- "Egypt": "Egypt",
393
- "Morocco": "Morocco",
394
- "Saudi Arabia": "Saudi Arabia",
395
- "United Arab Emirates": "United Arab Emirates",
396
- "Israel": "Israel"
397
- }
398
-
399
- # 아메리카 지역
400
- COUNTRY_LANGUAGES_AMERICA = {
401
- "United States": "en",
402
- "Canada": "en",
403
- "Mexico": "es",
404
- "Brazil": "pt",
405
- "Argentina": "es",
406
- "Chile": "es",
407
- "Colombia": "es",
408
- "Peru": "es",
409
- "Venezuela": "es"
410
- }
411
-
412
- COUNTRY_LOCATIONS_AMERICA = {
413
- "United States": "United States",
414
- "Canada": "Canada",
415
- "Mexico": "Mexico",
416
- "Brazil": "Brazil",
417
- "Argentina": "Argentina",
418
- "Chile": "Chile",
419
- "Colombia": "Colombia",
420
- "Peru": "Peru",
421
- "Venezuela": "Venezuela"
422
  }
423
 
424
- # 지역 선택 리스트
425
- REGIONS = [
426
- "동아시아",
427
- "동남아시아/오세아니아",
428
- "동유럽",
429
- "서유럽",
430
- "중동/아프리카",
431
- "아메리카"
432
- ]
433
-
434
 
435
  @lru_cache(maxsize=100)
436
  def translate_query(query, country):
@@ -468,35 +439,10 @@ def translate_query(query, country):
468
  return query
469
 
470
 
471
- @lru_cache(maxsize=200)
472
- def translate_to_korean(text):
473
- try:
474
- url = "https://translate.googleapis.com/translate_a/single"
475
- params = {
476
- "client": "gtx",
477
- "sl": "auto",
478
- "tl": "ko",
479
- "dt": "t",
480
- "q": text
481
- }
482
-
483
- session = requests.Session()
484
- retries = Retry(total=3, backoff_factor=0.5)
485
- session.mount('https://', HTTPAdapter(max_retries=retries))
486
-
487
- response = session.get(url, params=params, timeout=(5, 10))
488
- translated_text = response.json()[0][0][0]
489
- return translated_text
490
- except Exception as e:
491
- print(f"한글 번역 오류: {str(e)}")
492
- return text
493
-
494
  def is_english(text):
495
  return all(ord(char) < 128 for char in text.replace(' ', '').replace('-', '').replace('_', ''))
496
-
497
- def is_korean(text):
498
- return any('\uAC00' <= char <= '\uD7A3' for char in text)
499
-
500
  def search_serphouse(query, country, page=1, num_result=10):
501
  url = "https://api.serphouse.com/serp/live"
502
 
@@ -528,28 +474,24 @@ def search_serphouse(query, country, page=1, num_result=10):
528
  }
529
 
530
  try:
531
- # 세션 설정 개선
532
  session = requests.Session()
533
 
534
- # 재시도 설정 강화
535
  retries = Retry(
536
- total=5, # 최대 재시도 횟수 증가
537
- backoff_factor=1, # 재시도 간격 증가
538
- status_forcelist=[500, 502, 503, 504, 429], # 재시도할 HTTP 상태 코드
539
- allowed_methods=["POST"] # POST 요청에 대한 재시도 허용
540
  )
541
 
542
- # 타임아웃 설정 조정
543
  adapter = HTTPAdapter(max_retries=retries)
544
  session.mount('http://', adapter)
545
  session.mount('https://', adapter)
546
 
547
- # 타임아웃 값 증가 (connect timeout, read timeout)
548
  response = session.post(
549
  url,
550
  json=payload,
551
  headers=headers,
552
- timeout=(30, 30) # 연결 타임아웃 30초, 읽기 타임아웃 30초
553
  )
554
 
555
  response.raise_for_status()
@@ -571,6 +513,7 @@ def search_serphouse(query, country, page=1, num_result=10):
571
  "translated_query": query
572
  }
573
 
 
574
  def format_results_from_raw(response_data):
575
  if "error" in response_data:
576
  return "Error: " + response_data["error"], []
@@ -584,10 +527,14 @@ def format_results_from_raw(response_data):
584
  return "검색 결과가 없습니다.", []
585
 
586
  # 한국 도메인 및 한국 관련 키워드 필터링
587
- korean_domains = ['.kr', 'korea', 'korean', 'yonhap', 'hankyung', 'chosun',
588
- 'donga', 'joins', 'hani', 'koreatimes', 'koreaherald']
589
- korean_keywords = ['korea', 'korean', 'seoul', 'busan', 'incheon', 'daegu',
590
- 'gwangju', 'daejeon', 'ulsan', 'sejong']
 
 
 
 
591
 
592
  filtered_articles = []
593
  for idx, result in enumerate(news_results, 1):
@@ -596,8 +543,10 @@ def format_results_from_raw(response_data):
596
  channel = result.get("channel", result.get("source", "")).lower()
597
 
598
  # 한국 관련 컨텐츠 필터링
599
- is_korean_content = any(domain in url or domain in channel for domain in korean_domains) or \
600
- any(keyword in title.lower() for keyword in korean_keywords)
 
 
601
 
602
  if not is_korean_content:
603
  filtered_articles.append({
@@ -615,180 +564,12 @@ def format_results_from_raw(response_data):
615
  except Exception as e:
616
  return f"결과 처리 중 오류 발생: {str(e)}", []
617
 
 
618
  def serphouse_search(query, country):
619
  response_data = search_serphouse(query, country)
620
  return format_results_from_raw(response_data)
621
 
622
 
623
- def search_and_display(query, country, articles_state, progress=gr.Progress()):
624
- with ThreadPoolExecutor(max_workers=3) as executor:
625
- progress(0, desc="검색어 번역 중...")
626
- future_translation = executor.submit(translate_query, query, country)
627
- translated_query = future_translation.result()
628
- translated_display = f"**원본 검색어:** {query}\n**번역된 검색어:** {translated_query}" if translated_query != query else f"**검색어:** {query}"
629
-
630
- progress(0.3, desc="검색 중...")
631
- response_data = search_serphouse(query, country)
632
-
633
- progress(0.6, desc="결과 처리 중...")
634
- error_message, articles = format_results_from_raw(response_data)
635
-
636
- outputs = []
637
- outputs.append(gr.update(value="검색을 진행중입니다...", visible=True))
638
- outputs.append(gr.update(value=translated_display, visible=True))
639
-
640
- if error_message:
641
- outputs.append(gr.update(value=error_message, visible=True))
642
- for comp in article_components:
643
- outputs.extend([
644
- gr.update(visible=False), gr.update(), gr.update(),
645
- gr.update(), gr.update()
646
- ])
647
- articles_state = []
648
- else:
649
- outputs.append(gr.update(value="", visible=False))
650
- if not error_message and articles:
651
- futures = []
652
- for article in articles:
653
- future = executor.submit(translate_to_korean, article['snippet'])
654
- futures.append((article, future))
655
-
656
- progress(0.8, desc="번역 처리 중...")
657
- for article, future in futures:
658
- article['korean_summary'] = future.result()
659
-
660
- total_articles = len(articles)
661
- for idx, comp in enumerate(article_components):
662
- progress((idx + 1) / total_articles, desc=f"결과 표시 중... {idx + 1}/{total_articles}")
663
- if idx < len(articles):
664
- article = articles[idx]
665
- image_url = article['image_url']
666
- 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)
667
-
668
- outputs.extend([
669
- gr.update(visible=True),
670
- gr.update(value=f"### [{article['title']}]({article['link']})"),
671
- image_update,
672
- gr.update(value=f"**요약:** {article['snippet']}\n\n**한글 요약:** {article['korean_summary']}"),
673
- gr.update(value=f"**출처:** {article['channel']} | **시간:** {article['time']}")
674
- ])
675
- else:
676
- outputs.extend([
677
- gr.update(visible=False), gr.update(), gr.update(),
678
- gr.update(), gr.update()
679
- ])
680
- articles_state = articles
681
-
682
- progress(1.0, desc="완료!")
683
- outputs.append(articles_state)
684
- outputs[0] = gr.update(value="", visible=False)
685
-
686
- return outputs
687
-
688
- def get_region_countries(region):
689
- """선택된 지역의 국가 및 언어 정보 반환"""
690
- if region == "동아시아":
691
- return COUNTRY_LOCATIONS_EAST_ASIA, COUNTRY_LANGUAGES_EAST_ASIA
692
- elif region == "동남아시아/오세아니아":
693
- return COUNTRY_LOCATIONS_SOUTHEAST_ASIA_OCEANIA, COUNTRY_LANGUAGES_SOUTHEAST_ASIA_OCEANIA
694
- elif region == "동유럽":
695
- return COUNTRY_LOCATIONS_EAST_EUROPE, COUNTRY_LANGUAGES_EAST_EUROPE
696
- elif region == "서유럽":
697
- return COUNTRY_LOCATIONS_WEST_EUROPE, COUNTRY_LANGUAGES_WEST_EUROPE
698
- elif region == "중동/아프리카":
699
- return COUNTRY_LOCATIONS_ARAB_AFRICA, COUNTRY_LANGUAGES_ARAB_AFRICA
700
- elif region == "아메리카":
701
- return COUNTRY_LOCATIONS_AMERICA, COUNTRY_LANGUAGES_AMERICA
702
- return {}, {}
703
-
704
- def search_global(query, region, articles_state_global):
705
- """지역별 검색 함수"""
706
- status_msg = f"{region} 지역 검색을 시작합니다..."
707
- all_results = []
708
-
709
- outputs = [
710
- gr.update(value=status_msg, visible=True),
711
- gr.update(value=f"**검색어:** {query}", visible=True),
712
- ]
713
-
714
- for _ in global_article_components:
715
- outputs.extend([
716
- gr.update(visible=False), gr.update(), gr.update(),
717
- gr.update(), gr.update()
718
- ])
719
- outputs.append([])
720
-
721
- yield outputs
722
-
723
- # 선택된 지역의 국가 정보 가져오기
724
- locations, languages = get_region_countries(region)
725
- total_countries = len(locations)
726
-
727
- for idx, (country, location) in enumerate(locations.items(), 1):
728
- try:
729
- status_msg = f"{region} - {country} 검색 중... ({idx}/{total_countries} 국가)"
730
- outputs[0] = gr.update(value=status_msg, visible=True)
731
- yield outputs
732
-
733
- error_message, articles = serphouse_search(query, country)
734
- if not error_message and articles:
735
- for article in articles:
736
- article['source_country'] = country
737
- article['region'] = region
738
-
739
- all_results.extend(articles)
740
- sorted_results = sorted(all_results, key=lambda x: x.get('time', ''), reverse=True)
741
-
742
- seen_urls = set()
743
- unique_results = []
744
- for article in sorted_results:
745
- url = article.get('link', '')
746
- if url not in seen_urls:
747
- seen_urls.add(url)
748
- unique_results.append(article)
749
-
750
- unique_results = unique_results[:MAX_GLOBAL_RESULTS]
751
-
752
- outputs = [
753
- gr.update(value=f"{region} - {idx}/{total_countries} 국가 검색 완료\n현재까지 발견된 뉴스: {len(unique_results)}건", visible=True),
754
- gr.update(value=f"**검색어:** {query} | **지역:** {region}", visible=True),
755
- ]
756
-
757
- for idx, comp in enumerate(global_article_components):
758
- if idx < len(unique_results):
759
- article = unique_results[idx]
760
- image_url = article.get('image_url', '')
761
- 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)
762
-
763
- korean_summary = translate_to_korean(article['snippet'])
764
-
765
- outputs.extend([
766
- gr.update(visible=True),
767
- gr.update(value=f"### [{article['title']}]({article['link']})"),
768
- image_update,
769
- gr.update(value=f"**요약:** {article['snippet']}\n\n**한글 요약:** {korean_summary}"),
770
- gr.update(value=f"**출처:** {article['channel']} | **국가:** {article['source_country']} | **지역:** {article['region']} | **시간:** {article['time']}")
771
- ])
772
- else:
773
- outputs.extend([
774
- gr.update(visible=False),
775
- gr.update(),
776
- gr.update(),
777
- gr.update(),
778
- gr.update()
779
- ])
780
-
781
- outputs.append(unique_results)
782
- yield outputs
783
-
784
- except Exception as e:
785
- print(f"Error searching {country}: {str(e)}")
786
- continue
787
-
788
- final_status = f"{region} 검색 완료! 총 {len(unique_results)}개의 뉴스가 발견되었습니다."
789
- outputs[0] = gr.update(value=final_status, visible=True)
790
- yield outputs
791
-
792
  css = """
793
  /* 전역 스타일 */
794
  footer {visibility: hidden;}
@@ -867,7 +648,7 @@ footer {visibility: hidden;}
867
  z-index: 1000;
868
  }
869
 
870
- /* 프로그레스바 */
871
  .progress-bar {
872
  height: 100%;
873
  background: linear-gradient(90deg, #2196F3, #00BCD4);
@@ -965,454 +746,73 @@ footer {visibility: hidden;}
965
  """
966
 
967
 
968
- def get_article_content(url):
969
- try:
970
- headers = {
971
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
972
- }
973
- session = requests.Session()
974
- retries = Retry(total=3, backoff_factor=0.5)
975
- session.mount('https://', HTTPAdapter(max_retries=retries))
976
-
977
- response = session.get(url, headers=headers, timeout=30)
978
- response.raise_for_status()
979
- soup = BeautifulSoup(response.content, 'html.parser')
980
-
981
- # 메타 데이터 추출
982
- title = soup.find('meta', property='og:title') or soup.find('title')
983
- title = title.get('content', '') if hasattr(title, 'get') else title.string if title else ''
984
-
985
- description = soup.find('meta', property='og:description') or soup.find('meta', {'name': 'description'})
986
- description = description.get('content', '') if description else ''
987
-
988
- # 본문 추출 개선
989
- article_content = ''
990
-
991
- # 일반적인 기사 본문 컨테이너 검색
992
- content_selectors = [
993
- 'article', '.article-body', '.article-content', '#article-body',
994
- '.story-body', '.post-content', '.entry-content', '.content-body',
995
- '[itemprop="articleBody"]', '.story-content'
996
- ]
997
-
998
- for selector in content_selectors:
999
- content = soup.select_one(selector)
1000
- if content:
1001
- # 불필요한 요소 제거
1002
- for tag in content.find_all(['script', 'style', 'nav', 'header', 'footer', 'aside']):
1003
- tag.decompose()
1004
-
1005
- # 단락 추출
1006
- paragraphs = content.find_all(['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
1007
- if paragraphs:
1008
- article_content = '\n\n'.join([p.get_text().strip() for p in paragraphs if p.get_text().strip()])
1009
- break
1010
-
1011
- # 백업 방법: 모든 단락 추출
1012
- if not article_content:
1013
- paragraphs = soup.find_all('p')
1014
- article_content = '\n\n'.join([p.get_text().strip() for p in paragraphs if len(p.get_text().strip()) > 50])
1015
-
1016
- # 최종 콘텐츠 구성
1017
- full_content = f"Title: {title}\n\nDescription: {description}\n\nContent:\n{article_content}"
1018
-
1019
- # 텍스트 정제
1020
- full_content = re.sub(r'\s+', ' ', full_content) # 연속된 공백 제거
1021
- full_content = re.sub(r'\n\s*\n', '\n\n', full_content) # 연속된 빈 줄 제거
1022
-
1023
- return full_content.strip()
1024
-
1025
- except Exception as e:
1026
- print(f"Crawling error details: {str(e)}") # 디버깅을 위한 상세 에러 출력
1027
- return f"Error crawling content: {str(e)}"
1028
-
1029
- def respond(url, history, system_message, max_tokens, temperature, top_p):
1030
- if not url.startswith('http'):
1031
- history.append((url, "올바른 URL을 입력해주세요."))
1032
- return history
1033
-
1034
- try:
1035
- article_content = get_article_content(url)
1036
-
1037
- translation_prompt = f"""다음 영문 기사를 한국어로 번역하고 기사를 작성해주세요.
1038
-
1039
- 1단계: 전문 번역
1040
- ===번역 시작===
1041
- {article_content}
1042
- ===번역 끝===
1043
-
1044
- 2단계: 기사 작성 가이드라인
1045
- 다음 요구사항에 따라 한국어 기사를 작성하세요:
1046
-
1047
- 1. 구조
1048
- - 헤드라인: 핵심 내용을 담은 강력한 제목
1049
- - 부제목: 헤드라인 보완 설명
1050
- - 리드문: 기사의 핵심을 요약한 첫 문단
1051
- - 본문: 상세 내용 전개
1052
-
1053
- 2. ���성 규칙
1054
- - 객관적이고 정확한 사실 전달
1055
- - 문장은 '다.'로 종결
1056
- - 단락 간 자연스러운 흐름
1057
- - 인용구는 따옴표 처리
1058
- - 핵심 정보를 앞부분에 배치
1059
- - 전문 용어는 적절한 설명 추가
1060
-
1061
- 3. 형식
1062
- - 적절한 단락 구분
1063
- - 읽기 쉬운 문장 길이
1064
- - 논리적인 정보 구성
1065
-
1066
- 각 단계는 '===번역===', '===기사==='로 명확히 구분하여 출력하세요.
1067
- """
1068
-
1069
- messages = [
1070
- {
1071
- "role": "system",
1072
- "content": system_message
1073
- },
1074
- {"role": "user", "content": translation_prompt}
1075
- ]
1076
-
1077
- history.append((url, "번역 및 기사 작성을 시작합니다..."))
1078
-
1079
- full_response = ""
1080
- for message in client.chat.completions.create(
1081
- model="CohereForAI/c4ai-command-r-plus-08-2024",
1082
- max_tokens=max_tokens,
1083
- stream=True,
1084
- temperature=temperature,
1085
- top_p=top_p,
1086
- messages=messages,
1087
- ):
1088
- if hasattr(message.choices[0].delta, 'content'):
1089
- token = message.choices[0].delta.content
1090
- if token:
1091
- full_response += token
1092
- history[-1] = (url, full_response)
1093
- yield history
1094
-
1095
- except Exception as e:
1096
- error_message = f"처리 중 오류가 발생했습니다: {str(e)}"
1097
- history.append((url, error_message))
1098
- yield history
1099
-
1100
- return history
1101
-
1102
-
1103
- def continue_writing(history, system_message, max_tokens, temperature, top_p):
1104
- if not history:
1105
- return history
1106
-
1107
- last_response = history[-1][1] if history else ""
1108
- continue_prompt = f"""이전 내용을 이어서 계속 작성해주세요.
1109
- 마지막 응답: {last_response}
1110
-
1111
- 추가 지침:
1112
- 1. 이전 내용의 맥락을 유지하며 자연스럽게 이어서 작성
1113
- 2. 새로운 정보나 상세 설명을 추가
1114
- 3. 필요한 경우 보충 설명이나 분석 제공
1115
- 4. 기사 형식과 스타일 유지
1116
- 5. 필요한 경우 추가적인 이미지 프롬프트 생성
1117
- """
1118
-
1119
- # 메시지 구조 수정
1120
- messages = [
1121
- {"role": "system", "content": system_message},
1122
- {"role": "user", "content": continue_prompt} # 사용자 메시지로 시작
1123
- ]
1124
-
1125
- try:
1126
- full_response = ""
1127
- for message in client.chat.completions.create(
1128
- model="CohereForAI/c4ai-command-r-plus-08-2024",
1129
- max_tokens=max_tokens,
1130
- stream=True,
1131
- temperature=temperature,
1132
- top_p=top_p,
1133
- messages=messages,
1134
- ):
1135
- if hasattr(message.choices[0].delta, 'content'):
1136
- token = message.choices[0].delta.content
1137
- if token:
1138
- full_response += token
1139
- # 이전 대화 기록을 유지하면서 새로운 응답 추가
1140
- new_history = history.copy()
1141
- new_history.append(("계속 작성", full_response))
1142
- yield new_history
1143
-
1144
- except Exception as e:
1145
- error_message = f"계속 작성 중 오류가 발생했습니다: {str(e)}"
1146
- new_history = history.copy()
1147
- new_history.append(("오류", error_message))
1148
- yield new_history
1149
-
1150
- return history
1151
-
1152
  with gr.Blocks(theme="Yntec/HaleyCH_Theme_Orange", css=css, title="NewsAI 서비스") as iface:
1153
- init_db() # DB 초기화
1154
 
1155
  with gr.Tabs():
1156
- # DB 저장/불러오기
1157
  with gr.Tab("DB 검색"):
1158
- gr.Markdown("삼성/미국 검색 결과를 DB에 저장하고 불러옵니다.")
 
1159
 
 
1160
  with gr.Row():
1161
- search_button = gr.Button("검색: samsung/미국", variant="primary")
1162
- load_button = gr.Button("출력: samsung/미국", variant="secondary")
1163
-
1164
- results_display = gr.Markdown()
1165
 
1166
- # 버튼 이벤트 연결
1167
- search_button.click(
1168
- fn=search_samsung_us,
1169
- outputs=results_display
1170
  )
1171
 
1172
- load_button.click(
1173
- fn=load_samsung_us,
1174
- outputs=results_display
1175
- )
1176
-
1177
- with gr.Tab("국가별"):
1178
- gr.Markdown("검색어를 입력하고 원하는 국가(한국 제외)를를 선택하면, 검색어와 일치하는 24시간 이내 뉴스를 최대 100개 출력합니다.")
1179
- gr.Markdown("국가 선택후 검색어에 '한글'을 입력하면 현지 언어로 번역되어 검색합니다. 예: 'Taiwan' 국가 선택후 '삼성' 입력시 '三星'으로 자동 검색")
1180
-
1181
- with gr.Column():
1182
- with gr.Row():
1183
- query = gr.Textbox(label="검색어")
1184
- country = gr.Dropdown(
1185
- choices=sorted(list(COUNTRY_LOCATIONS.keys())),
1186
- label="국가",
1187
- value="United States"
1188
- )
1189
-
1190
- # Examples 추가
1191
- gr.Examples(
1192
- examples=[
1193
- "artificial intelligence",
1194
- "NVIDIA",
1195
- "OPENAI",
1196
- "META LLAMA",
1197
- "black forest labs",
1198
- "GOOGLE gemini",
1199
- "anthropic Claude",
1200
- "X.AI",
1201
- "HUGGINGFACE",
1202
- "HYNIX",
1203
- "Large Language model",
1204
- "CHATGPT",
1205
- "StabilityAI",
1206
- "MISTRALAI",
1207
- "QWEN",
1208
- "MIDJOURNEY",
1209
- "GPU"
1210
- ],
1211
- inputs=query,
1212
- label="자주 사용되는 검색어"
1213
- )
1214
-
1215
- status_message = gr.Markdown("", visible=True)
1216
- translated_query_display = gr.Markdown(visible=False)
1217
- search_button = gr.Button("검색", variant="primary")
1218
-
1219
- progress = gr.Progress()
1220
- articles_state = gr.State([])
1221
-
1222
- article_components = []
1223
- for i in range(100):
1224
- with gr.Group(visible=False) as article_group:
1225
- title = gr.Markdown()
1226
- image = gr.Image(width=200, height=150)
1227
- snippet = gr.Markdown()
1228
- info = gr.Markdown()
1229
-
1230
- article_components.append({
1231
- 'group': article_group,
1232
- 'title': title,
1233
- 'image': image,
1234
- 'snippet': snippet,
1235
- 'info': info,
1236
- 'index': i,
1237
- })
1238
-
1239
- # 전세계 탭
1240
- with gr.Tab("전세계"):
1241
- gr.Markdown("대륙별로 24시간 이내 뉴스를 검색합니다.")
1242
-
1243
  with gr.Column():
1244
- with gr.Column(elem_id="status_area"):
1245
  with gr.Row():
1246
- query_global = gr.Textbox(label="검색어")
1247
- region_select = gr.Dropdown(
1248
- choices=REGIONS,
1249
- label="지역 선택",
1250
- value="동아시아"
1251
- )
1252
- search_button_global = gr.Button("검색", variant="primary")
1253
-
1254
- status_message_global = gr.Markdown("")
1255
- translated_query_display_global = gr.Markdown("")
1256
-
1257
- with gr.Column(elem_id="results_area"):
1258
- articles_state_global = gr.State([])
1259
- global_article_components = []
1260
- for i in range(MAX_GLOBAL_RESULTS):
1261
- with gr.Group(visible=False) as article_group:
1262
- title = gr.Markdown()
1263
- image = gr.Image(width=200, height=150)
1264
- snippet = gr.Markdown()
1265
- info = gr.Markdown()
1266
-
1267
- global_article_components.append({
1268
- 'group': article_group,
1269
- 'title': title,
1270
- 'image': image,
1271
- 'snippet': snippet,
1272
- 'info': info,
1273
- 'index': i,
1274
- })
1275
-
1276
- # AI 번역 탭
1277
- with gr.Tab("AI 기사 생성"):
1278
- gr.Markdown("뉴스 URL을 입력하면 AI가 한국어로 번역하여 기사 형식으로 작성합니다.")
1279
- gr.Markdown("이미지 생성: https://huggingface.co/spaces/ginipick/FLUXllama ")
1280
-
1281
- with gr.Column():
1282
- chatbot = gr.Chatbot(height=600)
1283
-
1284
- with gr.Row():
1285
- url_input = gr.Textbox(
1286
- label="뉴스 URL",
1287
- placeholder="https://..."
1288
- )
1289
-
1290
- with gr.Row():
1291
- translate_button = gr.Button("기사 생성", variant="primary")
1292
- continue_button = gr.Button("계속 이어서 작성", variant="secondary")
1293
-
1294
- with gr.Accordion("고급 설정", open=False):
1295
- system_message = gr.Textbox(
1296
- value="""You are a professional translator and journalist. Follow these steps strictly:
1297
- 1. TRANSLATION
1298
- - Start with ===번역=== marker
1299
- - Provide accurate Korean translation
1300
- - Maintain original meaning and context
1301
- 2. ARTICLE WRITING
1302
- - Start with ===기사=== marker
1303
- - Write a new Korean news article based on the translation
1304
- - Follow newspaper article format
1305
- - Use formal news writing style
1306
- - End sentences with '다.'
1307
- - Include headline and subheadline
1308
- - Organize paragraphs clearly
1309
- - Put key information first
1310
- - Use quotes appropriately
1311
-
1312
- 3. IMAGE PROMPT GENERATION
1313
- - Start with ===이미지 프롬프트=== marker
1314
- - Create detailed Korean prompts for image generation
1315
- - Prompts should reflect the article's main theme and content
1316
- - Include key visual elements mentioned in the article
1317
- - Specify style, mood, and composition
1318
- - Format: "이미지 설명: [상세 설명]"
1319
- - Add style keywords: "스타일: [관련 키워드들]"
1320
- - Add mood keywords: "분위기: [관련 키워드들]"
1321
- IMPORTANT:
1322
- - Must complete all three steps in order
1323
- - Clearly separate each section with markers
1324
- - Never skip or combine steps
1325
- - Ensure image prompts align with article content""",
1326
- label="System message"
1327
- )
1328
-
1329
- max_tokens = gr.Slider(
1330
- minimum=1,
1331
- maximum=7800,
1332
- value=7624,
1333
- step=1,
1334
- label="Max new tokens"
1335
- )
1336
- temperature = gr.Slider(
1337
- minimum=0.1,
1338
- maximum=4.0,
1339
- value=0.7,
1340
- step=0.1,
1341
- label="Temperature"
1342
- )
1343
- top_p = gr.Slider(
1344
- minimum=0.1,
1345
- maximum=1.0,
1346
- value=0.95,
1347
- step=0.05,
1348
- label="Top-P"
1349
- )
1350
-
1351
- # 이벤트 연결 부분
1352
- # 국가별 탭 이벤트
1353
- search_outputs = [status_message, translated_query_display, gr.Markdown(visible=False)]
1354
- for comp in article_components:
1355
- search_outputs.extend([
1356
- comp['group'], comp['title'], comp['image'],
1357
- comp['snippet'], comp['info']
1358
- ])
1359
- search_outputs.append(articles_state)
1360
-
1361
- search_button.click(
1362
- fn=search_and_display,
1363
- inputs=[query, country, articles_state],
1364
- outputs=search_outputs,
1365
- show_progress=True
1366
- )
1367
-
1368
- # 전세계 탭 이벤트
1369
- global_search_outputs = [status_message_global, translated_query_display_global]
1370
- for comp in global_article_components:
1371
- global_search_outputs.extend([
1372
- comp['group'], comp['title'], comp['image'],
1373
- comp['snippet'], comp['info']
1374
- ])
1375
- global_search_outputs.append(articles_state_global)
1376
-
1377
- search_button_global.click(
1378
- fn=search_global,
1379
- inputs=[query_global, region_select, articles_state_global],
1380
- outputs=global_search_outputs,
1381
- show_progress=True
1382
- )
1383
-
1384
- # AI 번역 탭 이벤트
1385
- translate_button.click(
1386
- fn=respond,
1387
- inputs=[
1388
- url_input,
1389
- chatbot,
1390
- system_message,
1391
- max_tokens,
1392
- temperature,
1393
- top_p,
1394
- ],
1395
- outputs=chatbot
1396
- )
1397
-
1398
- # 계속 작성 버튼 이벤트
1399
- continue_button.click(
1400
- fn=continue_writing,
1401
- inputs=[
1402
- chatbot,
1403
- system_message,
1404
- max_tokens,
1405
- temperature,
1406
- top_p,
1407
- ],
1408
- outputs=chatbot
1409
- )
1410
 
1411
  iface.launch(
1412
  server_name="0.0.0.0",
1413
  server_port=7860,
1414
  share=True,
1415
- auth=("gini","pick"),
1416
  ssl_verify=False,
1417
  show_error=True
1418
- )
 
9
  from requests.packages.urllib3.util.retry import Retry
10
  from openai import OpenAI
11
  from bs4 import BeautifulSoup
12
+ import re
 
 
 
 
13
  import pathlib
14
+ import sqlite3
15
+ import pytz
16
+
17
+ # 한국 기업 리스트
18
+ KOREAN_COMPANIES = [
19
+ "NVIDIA",
20
+ "ALPHABET",
21
+ "APPLE",
22
+ "TESLA",
23
+ "AMAZON",
24
+ "MICROSOFT",
25
+ "META",
26
+ "INTEL",
27
+ "SAMSUNG",
28
+ "HYNIX",
29
+ "BITCOIN",
30
+ "crypto",
31
+ "stock",
32
+ "Economics",
33
+ "Finance",
34
+ "investing"
35
+ ]
36
+
37
+ def convert_to_seoul_time(timestamp_str):
38
+ try:
39
+ # 입력된 시간을 naive datetime 객체로 변환
40
+ dt = datetime.strptime(timestamp_str, '%Y-%m-%d %H:%M:%S')
41
+
42
+ # 서울 시간대 설정
43
+ seoul_tz = pytz.timezone('Asia/Seoul')
44
+
45
+ # 현재 시간을 서울 시간으로 인식하도록 수정
46
+ seoul_time = seoul_tz.localize(dt)
47
+
48
+ return seoul_time.strftime('%Y-%m-%d %H:%M:%S KST')
49
+ except Exception as e:
50
+ print(f"시간 변환 오류: {str(e)}")
51
+ return timestamp_str
52
+
53
+
54
+ def analyze_sentiment_batch(articles, client):
55
+ try:
56
+ # 모든 기사의 제목과 내용을 하나의 텍스트로 결합
57
+ combined_text = "\n\n".join([
58
+ f"제목: {article.get('title', '')}\n내용: {article.get('snippet', '')}"
59
+ for article in articles
60
+ ])
61
+
62
+ prompt = f\"\"\"다음 뉴스 모음에 대해 전반적인 감성 분석을 수행하세요:
63
+
64
+ 뉴스 내용:
65
+ {combined_text}
66
+
67
+ 다음 형식으로 분석해주세요:
68
+ 1. 전반적 감성: [긍정/부정/중립]
69
+ 2. 주요 긍정적 요소:
70
+ - [항목1]
71
+ - [항목2]
72
+ 3. 주요 부정적 요소:
73
+ - [항목1]
74
+ - [항목2]
75
+ 4. 종합 평가: [상세 설명]
76
+ \"\"\"
77
+
78
+ response = client.chat.completions.create(
79
+ model="CohereForAI/c4ai-command-r-plus-08-2024",
80
+ messages=[{"role": "user", "content": prompt}],
81
+ temperature=0.3,
82
+ max_tokens=1000
83
+ )
84
+
85
+ return response.choices[0].message.content
86
+ except Exception as e:
87
+ return f"감성 분석 실패: {str(e)}"
88
+
89
 
90
  # DB 초기화 함수
91
  def init_db():
 
101
  conn.commit()
102
  conn.close()
103
 
104
+
105
  def save_to_db(keyword, country, results):
106
  conn = sqlite3.connect("search_results.db")
107
  c = conn.cursor()
108
+
109
+ # 현재 시간을 서울 시간으로 가져오기
110
+ seoul_tz = pytz.timezone('Asia/Seoul')
111
+ now = datetime.now(seoul_tz)
112
+
113
+ # 시간대 정보를 제거하고 저장
114
+ timestamp = now.strftime('%Y-%m-%d %H:%M:%S')
115
+
116
+ c.execute("""INSERT INTO searches
117
+ (keyword, country, results, timestamp)
118
+ VALUES (?, ?, ?, ?)""",
119
+ (keyword, country, json.dumps(results), timestamp))
120
+
121
  conn.commit()
122
  conn.close()
123
 
124
+
125
  def load_from_db(keyword, country):
126
  conn = sqlite3.connect("search_results.db")
127
  c = conn.cursor()
 
130
  result = c.fetchone()
131
  conn.close()
132
  if result:
133
+ return json.loads(result[0]), convert_to_seoul_time(result[1])
134
  return None, None
135
 
 
 
 
 
 
 
 
136
 
 
 
 
 
 
 
 
 
137
  def display_results(articles):
138
  output = ""
139
  for idx, article in enumerate(articles, 1):
 
144
  output += f"요약: {article['snippet']}\n\n"
145
  return output
146
 
147
+
148
+ def search_company(company):
149
+ error_message, articles = serphouse_search(company, "United States")
150
+ if not error_message and articles:
151
+ save_to_db(company, "United States", articles)
152
+ return display_results(articles)
153
+ return f"{company}에 대한 검색 결과가 없습니다."
154
+
155
+
156
+ def load_company(company):
157
+ results, timestamp = load_from_db(company, "United States")
158
+ if results:
159
+ return f"### {company} 검색 결과\n저장 시간: {timestamp}\n\n" + display_results(results)
160
+ return f"{company}에 대한 저장된 결과가 없습니다."
161
+
162
+
163
+ def show_stats():
164
+ conn = sqlite3.connect("search_results.db")
165
+ c = conn.cursor()
166
+
167
+ output = "## 한국 기업 뉴스 분석 리포트\n\n"
168
+
169
+ for company in KOREAN_COMPANIES:
170
+ c.execute("""
171
+ SELECT results, timestamp
172
+ FROM searches
173
+ WHERE keyword = ?
174
+ ORDER BY timestamp DESC
175
+ LIMIT 1
176
+ """, (company,))
177
+
178
+ result = c.fetchone()
179
+ if result:
180
+ results_json, timestamp = result
181
+ articles = json.loads(results_json)
182
+ seoul_time = convert_to_seoul_time(timestamp)
183
+
184
+ output += f"### {company}\n"
185
+ output += f"- 마지막 업데이트: {seoul_time}\n"
186
+ output += f"- 저장된 기사 수: {len(articles)}건\n\n"
187
+
188
+ if articles:
189
+ # 전체 기사에 대한 감성 분석
190
+ sentiment_analysis = analyze_sentiment_batch(articles, client)
191
+ output += "#### 뉴스 감성 분석\n"
192
+ output += f"{sentiment_analysis}\n\n"
193
+
194
+ output += "---\n\n"
195
+
196
+ conn.close()
197
+ return output
198
+
199
+
200
+ def search_all_companies():
201
+ """
202
+ 등록된 모든 기업에 대해 순차적으로 검색 수행 후 결과를 합쳐서 반환
203
+ """
204
+ overall_result = "# [전체 검색 결과]\n\n"
205
+ for comp in KOREAN_COMPANIES:
206
+ overall_result += f"## {comp}\n"
207
+ overall_result += search_company(comp)
208
+ overall_result += "\n"
209
+ return overall_result
210
+
211
+ def load_all_companies():
212
+ """
213
+ 등록된 모든 기업에 대해 DB에 저장된 결과를 순차적으로 불러와서 합쳐서 반환
214
+ """
215
+ overall_result = "# [전체 출력 결과]\n\n"
216
+ for comp in KOREAN_COMPANIES:
217
+ overall_result += f"## {comp}\n"
218
+ overall_result += load_company(comp)
219
+ overall_result += "\n"
220
+ return overall_result
221
+
222
+ def full_summary_report():
223
+ """
224
+ '전체 분석 보고 요약' 버튼 클릭 시,
225
+ 1) 전체 검색 -> 2) 전체 출력 -> 3) 전체 통계(감성 분석)
226
+ 순으로 실행하고 그 결과를 한 번에 리턴
227
+ """
228
+ # 1) 전체 검색
229
+ search_result_text = search_all_companies()
230
+
231
+ # 2) 전체 출력
232
+ load_result_text = load_all_companies()
233
+
234
+ # 3) 전체 통계
235
+ stats_text = show_stats()
236
 
237
+ # 최종 보고서 형태로 합쳐서 반환
238
+ combined_report = (
239
+ "# 전체 분석 보고 요약\n\n"
240
+ "아래 순서로 실행되었습니다:\n"
241
+ "1. 모든 종목 검색 → 2. 모든 종목 DB 결과 출력 → 3. 전체 감성 분석 통계\n\n"
242
+ f"{search_result_text}\n\n"
243
+ f"{load_result_text}\n\n"
244
+ "## [전체 감성 분석 통계]\n\n"
245
+ f"{stats_text}"
246
+ )
247
+ return combined_report
248
+
249
+
250
  ACCESS_TOKEN = os.getenv("HF_TOKEN")
251
  if not ACCESS_TOKEN:
252
  raise ValueError("HF_TOKEN environment variable is not set")
 
256
  api_key=ACCESS_TOKEN,
257
  )
258
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  API_KEY = os.getenv("SERPHOUSE_API_KEY")
260
 
261
+
262
+ # 국가별 언어 코드 매핑 (첫 번째 탭에서는 'United States'만 주로 사용)
263
  COUNTRY_LANGUAGES = {
264
  "United States": "en",
265
  "KOREA": "ko",
 
392
  "Slovakia": "Slovakia",
393
  "Bulgaria": "Bulgaria",
394
  "Serbia": "Serbia",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
  "Estonia": "et",
396
  "Latvia": "lv",
397
  "Lithuania": "lt",
398
  "Slovenia": "sl",
399
+ "Luxembourg": "Luxembourg",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
  "Malta": "Malta",
401
  "Cyprus": "Cyprus",
402
+ "Iceland": "Iceland"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
  }
404
 
 
 
 
 
 
 
 
 
 
 
405
 
406
  @lru_cache(maxsize=100)
407
  def translate_query(query, country):
 
439
  return query
440
 
441
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
  def is_english(text):
443
  return all(ord(char) < 128 for char in text.replace(' ', '').replace('-', '').replace('_', ''))
444
+
445
+
 
 
446
  def search_serphouse(query, country, page=1, num_result=10):
447
  url = "https://api.serphouse.com/serp/live"
448
 
 
474
  }
475
 
476
  try:
 
477
  session = requests.Session()
478
 
 
479
  retries = Retry(
480
+ total=5,
481
+ backoff_factor=1,
482
+ status_forcelist=[500, 502, 503, 504, 429],
483
+ allowed_methods=["POST"]
484
  )
485
 
 
486
  adapter = HTTPAdapter(max_retries=retries)
487
  session.mount('http://', adapter)
488
  session.mount('https://', adapter)
489
 
 
490
  response = session.post(
491
  url,
492
  json=payload,
493
  headers=headers,
494
+ timeout=(30, 30)
495
  )
496
 
497
  response.raise_for_status()
 
513
  "translated_query": query
514
  }
515
 
516
+
517
  def format_results_from_raw(response_data):
518
  if "error" in response_data:
519
  return "Error: " + response_data["error"], []
 
527
  return "검색 결과가 없습니다.", []
528
 
529
  # 한국 도메인 및 한국 관련 키워드 필터링
530
+ korean_domains = [
531
+ '.kr', 'korea', 'korean', 'yonhap', 'hankyung', 'chosun',
532
+ 'donga', 'joins', 'hani', 'koreatimes', 'koreaherald'
533
+ ]
534
+ korean_keywords = [
535
+ 'korea', 'korean', 'seoul', 'busan', 'incheon', 'daegu',
536
+ 'gwangju', 'daejeon', 'ulsan', 'sejong'
537
+ ]
538
 
539
  filtered_articles = []
540
  for idx, result in enumerate(news_results, 1):
 
543
  channel = result.get("channel", result.get("source", "")).lower()
544
 
545
  # 한국 관련 컨텐츠 필터링
546
+ is_korean_content = (
547
+ any(domain in url or domain in channel for domain in korean_domains) or
548
+ any(keyword in title.lower() for keyword in korean_keywords)
549
+ )
550
 
551
  if not is_korean_content:
552
  filtered_articles.append({
 
564
  except Exception as e:
565
  return f"결과 처리 중 오류 발생: {str(e)}", []
566
 
567
+
568
  def serphouse_search(query, country):
569
  response_data = search_serphouse(query, country)
570
  return format_results_from_raw(response_data)
571
 
572
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
573
  css = """
574
  /* 전역 스타일 */
575
  footer {visibility: hidden;}
 
648
  z-index: 1000;
649
  }
650
 
651
+ /* 프로그레스bar */
652
  .progress-bar {
653
  height: 100%;
654
  background: linear-gradient(90deg, #2196F3, #00BCD4);
 
746
  """
747
 
748
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
749
  with gr.Blocks(theme="Yntec/HaleyCH_Theme_Orange", css=css, title="NewsAI 서비스") as iface:
750
+ init_db()
751
 
752
  with gr.Tabs():
753
+ # 번째 (DB 검색)
754
  with gr.Tab("DB 검색"):
755
+ gr.Markdown("## 한국 주요 기업 미국 뉴스 DB")
756
+ gr.Markdown("각 기업의 미국 뉴스를 검색하여 DB에 저장하고 불러올 수 있습니다.")
757
 
758
+ # (수정) 상단에 '전체 분석 보고 요약' 버튼을 배치
759
  with gr.Row():
760
+ full_report_btn = gr.Button("전체 분석 보고 요약", variant="primary")
761
+ full_report_display = gr.Markdown()
 
 
762
 
763
+ # 버튼 클릭 시 full_summary_report() 실행
764
+ full_report_btn.click(
765
+ fn=full_summary_report,
766
+ outputs=full_report_display
767
  )
768
 
769
+ # 이후 개별 기업 검색/출력 UI
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
770
  with gr.Column():
771
+ for i in range(0, len(KOREAN_COMPANIES), 2):
772
  with gr.Row():
773
+ # 왼쪽
774
+ with gr.Column():
775
+ company = KOREAN_COMPANIES[i]
776
+ with gr.Group():
777
+ gr.Markdown(f"### {company}")
778
+ with gr.Row():
779
+ search_btn = gr.Button("검색", variant="primary")
780
+ load_btn = gr.Button("출력", variant="secondary")
781
+ result_display = gr.Markdown()
782
+
783
+ search_btn.click(
784
+ fn=lambda c=company: search_company(c),
785
+ outputs=result_display
786
+ )
787
+ load_btn.click(
788
+ fn=lambda c=company: load_company(c),
789
+ outputs=result_display
790
+ )
791
+
792
+ # 오른쪽
793
+ if i + 1 < len(KOREAN_COMPANIES):
794
+ with gr.Column():
795
+ company = KOREAN_COMPANIES[i + 1]
796
+ with gr.Group():
797
+ gr.Markdown(f"### {company}")
798
+ with gr.Row():
799
+ search_btn = gr.Button("검색", variant="primary")
800
+ load_btn = gr.Button("출력", variant="secondary")
801
+ result_display = gr.Markdown()
802
+
803
+ search_btn.click(
804
+ fn=lambda c=company: search_company(c),
805
+ outputs=result_display
806
+ )
807
+ load_btn.click(
808
+ fn=lambda c=company: load_company(c),
809
+ outputs=result_display
810
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
811
 
812
  iface.launch(
813
  server_name="0.0.0.0",
814
  server_port=7860,
815
  share=True,
 
816
  ssl_verify=False,
817
  show_error=True
818
+ )