Spaces:
openfree
/
Running on CPU Upgrade

openfree commited on
Commit
69fda0a
ยท
verified ยท
1 Parent(s): dcba978

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +221 -633
app.py CHANGED
@@ -3,12 +3,12 @@ import requests
3
  import json
4
  import os
5
  from datetime import datetime, timedelta
6
- from huggingface_hub import InferenceClient
 
 
 
 
7
 
8
- from bs4 import BeautifulSoup
9
- import concurrent.futures
10
- import time
11
- import re
12
 
13
  MAX_COUNTRY_RESULTS = 100 # ๊ตญ๊ฐ€๋ณ„ ์ตœ๋Œ€ ๊ฒฐ๊ณผ ์ˆ˜
14
  MAX_GLOBAL_RESULTS = 1000 # ์ „์„ธ๊ณ„ ์ตœ๋Œ€ ๊ฒฐ๊ณผ ์ˆ˜
@@ -33,13 +33,12 @@ def create_article_components(max_results):
33
  return article_components
34
 
35
  API_KEY = os.getenv("SERPHOUSE_API_KEY")
36
- hf_client = InferenceClient("CohereForAI/c4ai-command-r-plus-08-2024", token=os.getenv("HF_TOKEN"))
37
 
38
  # ๊ตญ๊ฐ€๋ณ„ ์–ธ์–ด ์ฝ”๋“œ ๋งคํ•‘
39
  COUNTRY_LANGUAGES = {
40
  "United States": "en",
41
  "United Kingdom": "en",
42
- "Taiwan": "zh-TW", # ๋Œ€๋งŒ์–ด(๋ฒˆ์ฒด ์ค‘๊ตญ์–ด)
43
  "Canada": "en",
44
  "Australia": "en",
45
  "Germany": "de",
@@ -109,7 +108,7 @@ COUNTRY_LANGUAGES = {
109
  COUNTRY_LOCATIONS = {
110
  "United States": "United States",
111
  "United Kingdom": "United Kingdom",
112
- "Taiwan": "Taiwan", # ๊ตญ๊ฐ€๋ช… ์‚ฌ์šฉ
113
  "Canada": "Canada",
114
  "Australia": "Australia",
115
  "Germany": "Germany",
@@ -176,8 +175,7 @@ COUNTRY_LOCATIONS = {
176
  "Iceland": "Iceland"
177
  }
178
 
179
- MAJOR_COUNTRIES = list(COUNTRY_LOCATIONS.keys())
180
-
181
  # ๋™์•„์‹œ์•„ ์ง€์—ญ
182
  COUNTRY_LANGUAGES_EAST_ASIA = {
183
  "Taiwan": "zh-TW",
@@ -352,24 +350,20 @@ REGIONS = [
352
  "์•„๋ฉ”๋ฆฌ์นด"
353
  ]
354
 
 
 
355
  def translate_query(query, country):
356
  try:
357
- # ์˜์–ด ์ž…๋ ฅ ํ™•์ธ
358
  if is_english(query):
359
- print(f"์˜์–ด ๊ฒ€์ƒ‰์–ด ๊ฐ์ง€ - ์›๋ณธ ์‚ฌ์šฉ: {query}")
360
  return query
361
 
362
- # ์„ ํƒ๋œ ๊ตญ๊ฐ€๊ฐ€ ๋ฒˆ์—ญ ์ง€์› ๊ตญ๊ฐ€์ธ ๊ฒฝ์šฐ
363
  if country in COUNTRY_LANGUAGES:
364
- # South Korea ์„ ํƒ์‹œ ํ•œ๊ธ€ ์ž…๋ ฅ์€ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ
365
  if country == "South Korea":
366
- print(f"ํ•œ๊ตญ ์„ ํƒ - ์›๋ณธ ์‚ฌ์šฉ: {query}")
367
  return query
368
 
369
  target_lang = COUNTRY_LANGUAGES[country]
370
- print(f"๋ฒˆ์—ญ ์‹œ๋„: {query} -> {country}({target_lang})")
371
 
372
- url = f"https://translate.googleapis.com/translate_a/single"
373
  params = {
374
  "client": "gtx",
375
  "sl": "auto",
@@ -378,9 +372,12 @@ def translate_query(query, country):
378
  "q": query
379
  }
380
 
381
- response = requests.get(url, params=params)
 
 
 
 
382
  translated_text = response.json()[0][0][0]
383
- print(f"๋ฒˆ์—ญ ์™„๋ฃŒ: {query} -> {translated_text} ({country})")
384
  return translated_text
385
 
386
  return query
@@ -389,6 +386,8 @@ def translate_query(query, country):
389
  print(f"๋ฒˆ์—ญ ์˜ค๋ฅ˜: {str(e)}")
390
  return query
391
 
 
 
392
  def translate_to_korean(text):
393
  try:
394
  url = "https://translate.googleapis.com/translate_a/single"
@@ -400,7 +399,11 @@ def translate_to_korean(text):
400
  "q": text
401
  }
402
 
403
- response = requests.get(url, params=params)
 
 
 
 
404
  translated_text = response.json()[0][0][0]
405
  return translated_text
406
  except Exception as e:
@@ -421,8 +424,6 @@ def search_serphouse(query, country, page=1, num_result=10):
421
  date_range = f"{yesterday.strftime('%Y-%m-%d')},{now.strftime('%Y-%m-%d')}"
422
 
423
  translated_query = translate_query(query, country)
424
- print(f"Original query: {query}")
425
- print(f"Translated query: {translated_query}")
426
 
427
  payload = {
428
  "data": {
@@ -446,15 +447,18 @@ def search_serphouse(query, country, page=1, num_result=10):
446
  }
447
 
448
  try:
449
- response = requests.post(url, json=payload, headers=headers)
450
- print("Request payload:", json.dumps(payload, indent=2, ensure_ascii=False))
451
- print("Response status:", response.status_code)
452
 
 
453
  response.raise_for_status()
454
  return {"results": response.json(), "translated_query": translated_query}
455
  except requests.RequestException as e:
456
  return {"error": f"Error: {str(e)}", "translated_query": query}
457
 
 
 
458
  def format_results_from_raw(response_data):
459
  if "error" in response_data:
460
  return "Error: " + response_data["error"], []
@@ -488,546 +492,70 @@ def serphouse_search(query, country):
488
  return format_results_from_raw(response_data)
489
 
490
 
491
-
492
-
493
-
494
- # Hacker News API ๊ด€๋ จ ํ•จ์ˆ˜๋“ค ๋จผ์ € ์ถ”๊ฐ€
495
- def get_hn_item(item_id):
496
- """๊ฐœ๋ณ„ ์•„์ดํ…œ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ"""
497
- try:
498
- response = requests.get(f"https://hacker-news.firebaseio.com/v0/item/{item_id}.json")
499
- return response.json()
500
- except:
501
- return None
502
-
503
- def get_recent_stories():
504
- """์ตœ์‹  ์Šคํ† ๋ฆฌ ๊ฐ€์ ธ์˜ค๊ธฐ"""
505
- try:
506
- response = requests.get("https://hacker-news.firebaseio.com/v0/newstories.json")
507
- story_ids = response.json()
508
-
509
- recent_stories = []
510
- current_time = datetime.now().timestamp()
511
- day_ago = current_time - (24 * 60 * 60)
512
-
513
- for story_id in story_ids:
514
- story = get_hn_item(story_id)
515
- if story and 'time' in story and story['time'] > day_ago:
516
- recent_stories.append(story)
517
-
518
- if len(recent_stories) >= 100:
519
- break
520
-
521
- return recent_stories
522
- except Exception as e:
523
- print(f"Error fetching HN stories: {str(e)}")
524
- return []
525
-
526
- def format_hn_time(timestamp):
527
- """Unix timestamp๋ฅผ ์ฝ๊ธฐ ์‰ฌ์šด ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜"""
528
- try:
529
- dt = datetime.fromtimestamp(timestamp)
530
- return dt.strftime("%Y-%m-%d %H:%M:%S")
531
- except:
532
- return "Unknown time"
533
-
534
-
535
- def clean_text(text):
536
- """HTML ํƒœ๊ทธ ์ œ๊ฑฐ ๋ฐ ํ…์ŠคํŠธ ์ •๋ฆฌ"""
537
- text = re.sub(r'\s+', ' ', text)
538
- text = re.sub(r'<[^>]+>', '', text)
539
- return text.strip()
540
-
541
- def get_article_content(url):
542
- """URL์—์„œ ๊ธฐ์‚ฌ ๋‚ด์šฉ ์Šคํฌ๋ž˜ํ•‘"""
543
- if not url:
544
- return None
545
-
546
- # ์Šคํ‚ตํ•  ๋„๋ฉ”์ธ ๋ชฉ๋ก
547
- skip_domains = ['github.com', 'twitter.com', 'linkedin.com', 'facebook.com']
548
- if any(domain in url.lower() for domain in skip_domains):
549
- return None
550
-
551
- try:
552
- headers = {
553
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
554
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
555
- 'Accept-Language': 'en-US,en;q=0.5',
556
- 'Connection': 'keep-alive',
557
- }
558
 
559
- # ํƒ€์ž„์•„์›ƒ ์ฆ๊ฐ€ ๋ฐ ์žฌ์‹œ๋„ ์„ค์ •
560
- session = requests.Session()
561
- retries = requests.adapters.Retry(total=3, backoff_factor=1)
562
- session.mount('https://', requests.adapters.HTTPAdapter(max_retries=retries))
563
 
564
- response = session.get(url, headers=headers, timeout=15)
565
- response.raise_for_status()
566
 
567
- soup = BeautifulSoup(response.text, 'html.parser')
568
-
569
- # ๋ถˆํ•„์š”ํ•œ ์š”์†Œ ์ œ๊ฑฐ
570
- for tag in soup(['script', 'style', 'nav', 'footer', 'header', 'aside', 'iframe']):
571
- tag.decompose()
572
-
573
- # ๋ณธ๋ฌธ ๋‚ด์šฉ ์ถ”์ถœ
574
- article_text = ""
575
 
576
- # article ํƒœ๊ทธ ํ™•์ธ
577
- article = soup.find('article')
578
- if article:
579
- paragraphs = article.find_all('p')
 
 
 
 
580
  else:
581
- # main ํƒœ๊ทธ ํ™•์ธ
582
- main = soup.find('main')
583
- if main:
584
- paragraphs = main.find_all('p')
585
- else:
586
- # body์—์„œ ์ง์ ‘ ๊ฒ€์ƒ‰
587
- paragraphs = soup.find_all('p')
588
-
589
- text = ' '.join(p.get_text().strip() for p in paragraphs if p.get_text().strip())
590
- text = clean_text(text)
591
-
592
- if not text:
593
- return None
594
-
595
- return text[:4000] # ํ…์ŠคํŠธ ๊ธธ์ด ์ œํ•œ
596
-
597
- except Exception as e:
598
- print(f"Scraping error for {url}: {str(e)}")
599
- return None
600
-
601
- def generate_summary(text):
602
- """CohereForAI ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•œ ์š”์•ฝ ์ƒ์„ฑ"""
603
- if not text:
604
- return None
605
-
606
- prompt = """๋ฐ˜๋“œ์‹œ ํ•œ๊ธ€(ํ•œ๊ตญ์–ด)๋กœ ์ž‘์„ฑํ•˜๋ผ. Please analyze and summarize the following text in 2-3 sentences.
607
- Focus on the main points and key information:
608
-
609
- Text: {text}
610
-
611
- Summary:"""
612
-
613
- try:
614
- response = hf_client.text_generation(
615
- prompt.format(text=text),
616
- max_new_tokens=500,
617
- temperature=0.5,
618
- repetition_penalty=1.2
619
- )
620
- return response
621
- except Exception as e:
622
- print(f"Summary generation error: {str(e)}")
623
- return None
624
-
625
- def process_hn_story(story, progress=None):
626
- """๊ฐœ๋ณ„ ์Šคํ† ๋ฆฌ ์ฒ˜๋ฆฌ ๋ฐ ์š”์•ฝ"""
627
- try:
628
- url = story.get('url')
629
- if not url:
630
- return None # ์Šคํ‚ตํ•  ์Šคํ† ๋ฆฌ
631
-
632
- content = get_article_content(url)
633
- if not content:
634
- return None # ์Šคํฌ๋ž˜ํ•‘ ์‹คํŒจํ•œ ์Šคํ† ๋ฆฌ ์Šคํ‚ต
635
-
636
- summary_en = generate_summary(content)
637
- if not summary_en:
638
- return None # ์š”์•ฝ ์‹คํŒจํ•œ ์Šคํ† ๋ฆฌ ์Šคํ‚ต
639
-
640
- summary_ko = translate_to_korean(summary_en)
641
- if not summary_ko:
642
- return None # ๋ฒˆ์—ญ ์‹คํŒจํ•œ ์Šคํ† ๋ฆฌ ์Šคํ‚ต
643
-
644
- return {
645
- 'story': story,
646
- 'summary': summary_ko
647
- }
648
-
649
- except Exception as e:
650
- print(f"Story processing error: {str(e)}")
651
- return None # ์—๋Ÿฌ ๋ฐœ์ƒํ•œ ์Šคํ† ๋ฆฌ ์Šคํ‚ต
652
-
653
- def refresh_hn_stories():
654
- """Hacker News ์Šคํ† ๋ฆฌ ์ƒˆ๋กœ๊ณ ์นจ (์‹ค์‹œ๊ฐ„ ์ถœ๋ ฅ ๋ฒ„์ „)"""
655
- status_msg = "Hacker News ํฌ์ŠคํŠธ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ์ค‘..."
656
- outputs = [gr.update(value=status_msg, visible=True)]
657
-
658
- # ์ปดํฌ๋„ŒํŠธ ์ดˆ๊ธฐํ™”
659
- for comp in hn_article_components:
660
- outputs.extend([
661
- gr.update(visible=False),
662
- gr.update(),
663
- gr.update(),
664
- gr.update(visible=False), # report_button
665
- gr.update(visible=False), # report_content
666
- gr.update(visible=False) # show_report
667
- ])
668
-
669
- yield outputs
670
-
671
- # ์ตœ์‹  ์Šคํ† ๋ฆฌ ๊ฐ€์ ธ์˜ค๊ธฐ
672
- stories = get_recent_stories()
673
- processed_count = 0
674
- valid_stories = [] # ์„ฑ๊ณต์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋œ ์Šคํ† ๋ฆฌ ์ €์žฅ
675
-
676
- with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
677
- future_to_story = {executor.submit(process_hn_story, story): story
678
- for story in stories[:100]}
679
-
680
- for future in concurrent.futures.as_completed(future_to_story):
681
- processed_count += 1
682
- result = future.result()
683
-
684
- if result: # ์„ฑ๊ณต์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋œ ์Šคํ† ๋ฆฌ๋งŒ ์ถ”๊ฐ€
685
- valid_stories.append((result['story'], result['summary']))
686
 
687
- # ํ˜„์žฌ๊นŒ์ง€์˜ ๊ฒฐ๊ณผ ์ถœ๋ ฅ
688
- outputs = [gr.update(value=f"์ฒ˜๋ฆฌ ์ค‘... ({len(valid_stories)}/{processed_count} ์„ฑ๊ณต)", visible=True)]
 
689
 
690
- # ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ
691
- for idx, comp in enumerate(hn_article_components):
692
- if idx < len(valid_stories):
693
- story, summary = valid_stories[idx]
 
 
 
 
694
  outputs.extend([
695
  gr.update(visible=True),
696
- gr.update(value=f"### [{story.get('title', 'Untitled')}]({story.get('url', '#')})"),
697
- gr.update(value=f"""
698
- **์ž‘์„ฑ์ž:** {story.get('by', 'unknown')} |
699
- **์‹œ๊ฐ„:** {format_hn_time(story.get('time', 0))} |
700
- **์ ์ˆ˜:** {story.get('score', 0)} |
701
- **๋Œ“๊ธ€:** {len(story.get('kids', []))}๊ฐœ\n
702
- **AI ์š”์•ฝ:** {summary}
703
- """),
704
- gr.update(visible=True), # report_button
705
- gr.update(visible=False), # report_content
706
- gr.update(visible=False) # show_report
707
  ])
708
  else:
709
  outputs.extend([
710
- gr.update(visible=False),
711
- gr.update(),
712
- gr.update(),
713
- gr.update(visible=False),
714
- gr.update(visible=False),
715
- gr.update(visible=False)
716
  ])
717
-
718
- yield outputs
719
-
720
- # ์ตœ์ข… ์ƒํƒœ ์—…๋ฐ์ดํŠธ
721
- final_outputs = [gr.update(value=f"์ด {len(valid_stories)}๊ฐœ์˜ ํฌ์ŠคํŠธ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. (์ „์ฒด ์‹œ๋„: {processed_count})", visible=True)]
722
-
723
- for idx, comp in enumerate(hn_article_components):
724
- if idx < len(valid_stories):
725
- story, summary = valid_stories[idx]
726
- final_outputs.extend([
727
- gr.update(visible=True),
728
- gr.update(value=f"### [{story.get('title', 'Untitled')}]({story.get('url', '#')})"),
729
- gr.update(value=f"""
730
- **์ž‘์„ฑ์ž:** {story.get('by', 'unknown')} |
731
- **์‹œ๊ฐ„:** {format_hn_time(story.get('time', 0))} |
732
- **์ ์ˆ˜:** {story.get('score', 0)} |
733
- **๋Œ“๊ธ€:** {len(story.get('kids', []))}๊ฐœ\n
734
- **AI ์š”์•ฝ:** {summary}
735
- """),
736
- gr.update(visible=True), # report_button
737
- gr.update(visible=False), # report_content
738
- gr.update(visible=False) # show_report
739
- ])
740
- else:
741
- final_outputs.extend([
742
- gr.update(visible=False),
743
- gr.update(),
744
- gr.update(),
745
- gr.update(visible=False),
746
- gr.update(visible=False),
747
- gr.update(visible=False)
748
- ])
749
-
750
- yield final_outputs
751
 
752
- def generate_report(title, info, progress=gr.Progress()):
753
- """๋ฆฌํฌํŒ… ์ƒ์„ฑ"""
754
- try:
755
- progress(0.1, desc="๋ฆฌํฌํŒ… ์ƒ์„ฑ ์ค€๋น„ ์ค‘...")
756
-
757
- # HTML ํƒœ๊ทธ ์ œ๊ฑฐ ๋ฐ ํ…์ŠคํŠธ ์ถ”์ถœ
758
- title_text = re.sub(r'#*\s*\[(.*?)\].*', r'\1', title)
759
- info_text = re.sub(r'\*\*(.*?)\*\*|\n|AI ์š”์•ฝ:|์ž‘์„ฑ์ž:|์‹œ๊ฐ„:|์ ์ˆ˜:|๋Œ“๊ธ€:', ' ', info)
760
- info_text = ' '.join(info_text.split())
761
-
762
- progress(0.3, desc="ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ ์ค‘...")
763
 
764
- prompt = f"""๋„ˆ๋Š” Hacker News ํฌ์ŠคํŠธ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณด๋„ ๊ธฐ์‚ฌ ํ˜•ํƒœ์˜ ๋ฆฌํฌํŒ…์„ ์ž‘์„ฑํ•˜๋Š” ์—ญํ• ์ด๋‹ค.
765
- ๋„ˆ๋Š” ๋ฐ˜๋“œ์‹œ ํ•œ๊ธ€๋กœ ๋ฆฌํฌํŒ… ํ˜•์‹์˜ ๊ฐ๊ด€์  ๊ธฐ์‚ฌ ํ˜•ํƒœ๋กœ ์ž‘์„ฑํ•˜์—ฌ์•ผ ํ•œ๋‹ค.
766
- ์ƒ์„ฑ์‹œ 6ํ•˜์›์น™์— ์ž…๊ฐํ•˜๊ณ  ๊ธธ์ด๋Š” 4000ํ† ํฐ์„ ๋„˜์ง€ ์•Š์„๊ฒƒ.
767
- ๋„ˆ์˜ ์ถœ์ฒ˜๋‚˜ ๋ชจ๋ธ, ์ง€์‹œ๋ฌธ ๋“ฑ์„ ๋…ธ์ถœํ•˜์ง€ ๋ง๊ฒƒ
768
-
769
- ์ œ๋ชฉ: {title_text}
770
- ๋‚ด์šฉ: {info_text}
771
- """
772
-
773
- progress(0.5, desc="AI ๋ชจ๋ธ ์ฒ˜๋ฆฌ ์ค‘...")
774
-
775
- try:
776
- response = hf_client.text_generation(
777
- prompt,
778
- max_new_tokens=2000,
779
- temperature=0.7,
780
- repetition_penalty=1.2,
781
- return_full_text=False
782
- )
783
-
784
- progress(1.0, desc="์™„๋ฃŒ!")
785
-
786
- if response:
787
- formatted_response = f"### AI ๋ฆฌํฌํŒ…\n\n{response}"
788
- return [
789
- gr.update(value=formatted_response, visible=True), # report_content
790
- gr.update(value="์ ‘๊ธฐ", visible=True) # show_report
791
- ]
792
-
793
- except Exception as e:
794
- print(f"Model error: {str(e)}")
795
- time.sleep(2) # ์ž ์‹œ ๋Œ€๊ธฐ
796
-
797
- return [
798
- gr.update(value="๋ฆฌํฌํŒ… ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.", visible=True),
799
- gr.update(value="์ ‘๊ธฐ", visible=True)
800
- ]
801
-
802
- except Exception as e:
803
- print(f"Report generation error: {str(e)}")
804
- return [
805
- gr.update(value="๋ฆฌํฌํŒ… ์ƒ๏ฟฝ๏ฟฝ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", visible=True),
806
- gr.update(value="์ ‘๊ธฐ", visible=True)
807
- ]
808
-
809
- def toggle_report(report_content, show_report):
810
- """๋ฆฌํฌํŠธ ํ‘œ์‹œ/์ˆจ๊น€ ํ† ๊ธ€"""
811
- try:
812
- is_visible = report_content.visible
813
- return [
814
- gr.update(visible=not is_visible), # report_content
815
- gr.update(value="์ ‘๊ธฐ" if not is_visible else "ํŽผ์ณ ๋ณด๊ธฐ") # show_report
816
- ]
817
- except AttributeError:
818
- # report_content๊ฐ€ ๋ฌธ์ž์—ด์ธ ๊ฒฝ์šฐ
819
- return [
820
- gr.update(visible=True), # report_content
821
- gr.update(value="์ ‘๊ธฐ") # show_report
822
- ]
823
-
824
- css = """
825
- /* ์ „์—ญ ์Šคํƒ€์ผ */
826
- footer {visibility: hidden;}
827
-
828
- /* ๋ ˆ์ด์•„์›ƒ ์ปจํ…Œ์ด๋„ˆ */
829
- #status_area {
830
- background: rgba(255, 255, 255, 0.9);
831
- padding: 15px;
832
- border-bottom: 1px solid #ddd;
833
- margin-bottom: 20px;
834
- box-shadow: 0 2px 5px rgba(0,0,0,0.1);
835
- }
836
-
837
- #results_area {
838
- padding: 10px;
839
- margin-top: 10px;
840
- }
841
-
842
- /* ํƒญ ์Šคํƒ€์ผ */
843
- .tabs {
844
- border-bottom: 2px solid #ddd !important;
845
- margin-bottom: 20px !important;
846
- }
847
-
848
- .tab-nav {
849
- border-bottom: none !important;
850
- margin-bottom: 0 !important;
851
- }
852
-
853
- .tab-nav button {
854
- font-weight: bold !important;
855
- padding: 10px 20px !important;
856
- }
857
-
858
- .tab-nav button.selected {
859
- border-bottom: 2px solid #1f77b4 !important;
860
- color: #1f77b4 !important;
861
- }
862
-
863
- /* ์ƒํƒœ ๋ฉ”์‹œ์ง€ */
864
- #status_area .markdown-text {
865
- font-size: 1.1em;
866
- color: #2c3e50;
867
- padding: 10px 0;
868
- }
869
-
870
- /* ๊ธฐ๋ณธ ์ปจํ…Œ์ด๋„ˆ */
871
- .group {
872
- border: 1px solid #eee;
873
- padding: 15px;
874
- margin-bottom: 15px;
875
- border-radius: 5px;
876
- background: white;
877
- }
878
-
879
- /* ๋ฒ„ํŠผ ์Šคํƒ€์ผ */
880
- .primary-btn {
881
- background: #1f77b4 !important;
882
- border: none !important;
883
- }
884
-
885
- /* ์ž…๋ ฅ ํ•„๋“œ */
886
- .textbox {
887
- border: 1px solid #ddd !important;
888
- border-radius: 4px !important;
889
- }
890
-
891
- /* Hacker News ์•„ํ‹ฐํด ์Šคํƒ€์ผ */
892
- .hn-article-group {
893
- height: auto !important;
894
- min-height: 250px;
895
- margin-bottom: 20px;
896
- padding: 15px;
897
- border: 1px solid #eee;
898
- border-radius: 5px;
899
- background: white;
900
- box-shadow: 0 1px 3px rgba(0,0,0,0.05);
901
- }
902
-
903
- /* ๋ฆฌํฌํŠธ ์„น์…˜ ์Šคํƒ€์ผ */
904
- .report-section {
905
- margin-top: 15px;
906
- padding: 15px;
907
- border-top: 1px solid #eee;
908
- background: #f9f9f9;
909
- border-radius: 4px;
910
- }
911
-
912
- .report-content {
913
- margin-top: 15px;
914
- padding: 15px;
915
- border-top: 1px solid #eee;
916
- background: #f9f9f9;
917
- border-radius: 4px;
918
- font-size: 0.95em;
919
- line-height: 1.6;
920
- }
921
-
922
- /* ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ” */
923
- .progress {
924
- position: fixed;
925
- top: 0;
926
- left: 0;
927
- width: 100%;
928
- height: 4px;
929
- background: #f0f0f0;
930
- z-index: 1000;
931
- }
932
-
933
- .progress-bar {
934
- height: 100%;
935
- background: #1f77b4;
936
- transition: width 0.3s ease;
937
- position: fixed;
938
- top: 0;
939
- left: 0;
940
- width: 100%;
941
- z-index: 1000;
942
- }
943
-
944
- /* ๋ฆฌํฌํŠธ ์ฝ˜ํ…์ธ  ํ† ๊ธ€ */
945
- .hn-article-group .report-content {
946
- display: none;
947
- margin-top: 15px;
948
- padding: 15px;
949
- border-top: 1px solid #eee;
950
- background: #f9f9f9;
951
- transition: all 0.3s ease;
952
- }
953
-
954
- .hn-article-group .report-content.visible {
955
- display: block;
956
- }
957
-
958
- /* ๋ฐ˜์‘ํ˜• ๋””์ž์ธ */
959
- @media (max-width: 768px) {
960
- .hn-article-group {
961
- padding: 10px;
962
- margin-bottom: 15px;
963
- }
964
-
965
- .report-content {
966
- padding: 10px;
967
- }
968
- }
969
- """
970
-
971
-
972
-
973
-
974
-
975
- # ๊ธฐ์กด ํ•จ์ˆ˜๋“ค
976
- def search_and_display(query, country, articles_state, progress=gr.Progress()):
977
- status_msg = "๊ฒ€์ƒ‰์„ ์ง„ํ–‰์ค‘์ž…๋‹ˆ๋‹ค. ์ž ์‹œ๋งŒ ๊ธฐ๋‹ค๋ฆฌ์„ธ์š”..."
978
-
979
- progress(0, desc="๊ฒ€์ƒ‰์–ด ๋ฒˆ์—ญ ์ค‘...")
980
- translated_query = translate_query(query, country)
981
- translated_display = f"**์›๋ณธ ๊ฒ€์ƒ‰์–ด:** {query}\n**๋ฒˆ์—ญ๋œ ๊ฒ€์ƒ‰์–ด:** {translated_query}" if translated_query != query else f"**๊ฒ€์ƒ‰์–ด:** {query}"
982
-
983
- progress(0.2, desc="๊ฒ€์ƒ‰ ์‹œ์ž‘...")
984
- error_message, articles = serphouse_search(query, country)
985
- progress(0.5, desc="๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ ์ค‘...")
986
-
987
- outputs = []
988
- outputs.append(gr.update(value=status_msg, visible=True))
989
- outputs.append(gr.update(value=translated_display, visible=True))
990
-
991
- if error_message:
992
- outputs.append(gr.update(value=error_message, visible=True))
993
- for comp in article_components:
994
- outputs.extend([
995
- gr.update(visible=False), gr.update(), gr.update(),
996
- gr.update(), gr.update()
997
- ])
998
- articles_state = []
999
- else:
1000
- outputs.append(gr.update(value="", visible=False))
1001
- total_articles = len(articles)
1002
- for idx, comp in enumerate(article_components):
1003
- progress((idx + 1) / total_articles, desc=f"๊ฒฐ๊ณผ ํ‘œ์‹œ ์ค‘... {idx + 1}/{total_articles}")
1004
- if idx < len(articles):
1005
- article = articles[idx]
1006
- image_url = article['image_url']
1007
- 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)
1008
-
1009
- korean_summary = translate_to_korean(article['snippet'])
1010
-
1011
- outputs.extend([
1012
- gr.update(visible=True),
1013
- gr.update(value=f"### [{article['title']}]({article['link']})"),
1014
- image_update,
1015
- gr.update(value=f"**์š”์•ฝ:** {article['snippet']}\n\n**ํ•œ๊ธ€ ์š”์•ฝ:** {korean_summary}"),
1016
- gr.update(value=f"**์ถœ์ฒ˜:** {article['channel']} | **์‹œ๊ฐ„:** {article['time']}")
1017
- ])
1018
- else:
1019
- outputs.extend([
1020
- gr.update(visible=False), gr.update(), gr.update(),
1021
- gr.update(), gr.update()
1022
- ])
1023
- articles_state = articles
1024
-
1025
- progress(1.0, desc="์™„๋ฃŒ!")
1026
- outputs.append(articles_state)
1027
- outputs[0] = gr.update(value="", visible=False)
1028
-
1029
- return outputs
1030
-
1031
 
1032
  def get_region_countries(region):
1033
  """์„ ํƒ๋œ ์ง€์—ญ์˜ ๊ตญ๊ฐ€ ๋ฐ ์–ธ์–ด ์ •๋ณด ๋ฐ˜ํ™˜"""
@@ -1133,13 +661,153 @@ def search_global(query, region, articles_state_global):
1133
  outputs[0] = gr.update(value=final_status, visible=True)
1134
  yield outputs
1135
 
 
 
 
1136
 
1137
-
1138
-
 
 
 
 
 
 
1139
 
 
 
 
 
1140
 
 
 
 
 
 
1141
 
 
 
 
 
1142
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1143
 
1144
  with gr.Blocks(theme="Nymbo/Nymbo_Theme", css=css, title="NewsAI ์„œ๋น„์Šค") as iface:
1145
  with gr.Tabs():
@@ -1218,36 +886,6 @@ with gr.Blocks(theme="Nymbo/Nymbo_Theme", css=css, title="NewsAI ์„œ๋น„์Šค") as
1218
  'index': i,
1219
  })
1220
 
1221
- # AI ๋ฆฌํฌํ„ฐ ํƒญ
1222
- with gr.Tab("AI ๋ฆฌํฌํ„ฐ"):
1223
- gr.Markdown("์ง€๋‚œ 24์‹œ๊ฐ„ ๋™์•ˆ์˜ Hacker News ํฌ์ŠคํŠธ๋ฅผ AI๊ฐ€ ์š”์•ฝํ•˜์—ฌ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.")
1224
-
1225
- with gr.Column():
1226
- refresh_button = gr.Button("์ƒˆ๋กœ๊ณ ์นจ", variant="primary")
1227
- status_message_hn = gr.Markdown("")
1228
-
1229
- with gr.Column(elem_id="hn_results_area"):
1230
- hn_articles_state = gr.State([])
1231
- hn_article_components = []
1232
- for i in range(100):
1233
- with gr.Group(visible=False, elem_classes="hn-article-group") as article_group:
1234
- title = gr.Markdown()
1235
- info = gr.Markdown()
1236
- with gr.Row():
1237
- report_button = gr.Button("๋ฆฌํฌํŒ… ์ƒ์„ฑ", size="sm", variant="primary")
1238
- show_report = gr.Button("ํŽผ์ณ ๋ณด๊ธฐ", size="sm", visible=False)
1239
- report_content = gr.Markdown(visible=False)
1240
-
1241
- hn_article_components.append({
1242
- 'group': article_group,
1243
- 'title': title,
1244
- 'info': info,
1245
- 'report_button': report_button,
1246
- 'show_report': show_report,
1247
- 'report_content': report_content,
1248
- 'index': i,
1249
- })
1250
-
1251
  # ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ ๋ถ€๋ถ„
1252
  # ๊ตญ๊ฐ€๋ณ„ ํƒญ ์ด๋ฒคํŠธ
1253
  search_outputs = [status_message, translated_query_display, gr.Markdown(visible=False)]
@@ -1281,56 +919,6 @@ with gr.Blocks(theme="Nymbo/Nymbo_Theme", css=css, title="NewsAI ์„œ๋น„์Šค") as
1281
  show_progress=True
1282
  )
1283
 
1284
- # AI ๋ฆฌํฌํ„ฐ ํƒญ ์ด๋ฒคํŠธ
1285
- hn_outputs = [status_message_hn]
1286
- for comp in hn_article_components:
1287
- hn_outputs.extend([
1288
- comp['group'],
1289
- comp['title'],
1290
- comp['info'],
1291
- comp['report_button'],
1292
- comp['report_content'],
1293
- comp['show_report']
1294
- ])
1295
-
1296
- # ๊ฐ ์ปดํฌ๋„ŒํŠธ๋ณ„ ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ
1297
- for comp in hn_article_components:
1298
- # ๋ฆฌํฌํŒ… ์ƒ์„ฑ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ
1299
- comp['report_button'].click(
1300
- fn=generate_report,
1301
- inputs=[
1302
- comp['title'],
1303
- comp['info']
1304
- ],
1305
- outputs=[
1306
- comp['report_content'],
1307
- comp['show_report']
1308
- ],
1309
- api_name=f"generate_report_{comp['index']}",
1310
- show_progress=True
1311
- )
1312
-
1313
- # ํŽผ์ณ๋ณด๊ธฐ/์ ‘๊ธฐ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ
1314
- comp['show_report'].click(
1315
- fn=toggle_report,
1316
- inputs=[
1317
- comp['report_content'],
1318
- comp['show_report']
1319
- ],
1320
- outputs=[
1321
- comp['report_content'],
1322
- comp['show_report']
1323
- ],
1324
- api_name=f"toggle_report_{comp['index']}"
1325
- )
1326
-
1327
- # ์ƒˆ๋กœ๊ณ ์นจ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ
1328
- refresh_button.click(
1329
- fn=refresh_hn_stories,
1330
- outputs=hn_outputs,
1331
- show_progress=True
1332
- )
1333
-
1334
  iface.launch(
1335
  server_name="0.0.0.0",
1336
  server_port=7860,
@@ -1338,4 +926,4 @@ iface.launch(
1338
  auth=("it1","chosun1"),
1339
  ssl_verify=False,
1340
  show_error=True
1341
- )
 
3
  import json
4
  import os
5
  from datetime import datetime, timedelta
6
+ from concurrent.futures import ThreadPoolExecutor
7
+ from functools import lru_cache
8
+ from requests.adapters import HTTPAdapter
9
+ from requests.packages.urllib3.util.retry import Retry
10
+
11
 
 
 
 
 
12
 
13
  MAX_COUNTRY_RESULTS = 100 # ๊ตญ๊ฐ€๋ณ„ ์ตœ๋Œ€ ๊ฒฐ๊ณผ ์ˆ˜
14
  MAX_GLOBAL_RESULTS = 1000 # ์ „์„ธ๊ณ„ ์ตœ๋Œ€ ๊ฒฐ๊ณผ ์ˆ˜
 
33
  return article_components
34
 
35
  API_KEY = os.getenv("SERPHOUSE_API_KEY")
 
36
 
37
  # ๊ตญ๊ฐ€๋ณ„ ์–ธ์–ด ์ฝ”๋“œ ๋งคํ•‘
38
  COUNTRY_LANGUAGES = {
39
  "United States": "en",
40
  "United Kingdom": "en",
41
+ "Taiwan": "zh-TW",
42
  "Canada": "en",
43
  "Australia": "en",
44
  "Germany": "de",
 
108
  COUNTRY_LOCATIONS = {
109
  "United States": "United States",
110
  "United Kingdom": "United Kingdom",
111
+ "Taiwan": "Taiwan",
112
  "Canada": "Canada",
113
  "Australia": "Australia",
114
  "Germany": "Germany",
 
175
  "Iceland": "Iceland"
176
  }
177
 
178
+ # ์ง€์—ญ ์ •์˜
 
179
  # ๋™์•„์‹œ์•„ ์ง€์—ญ
180
  COUNTRY_LANGUAGES_EAST_ASIA = {
181
  "Taiwan": "zh-TW",
 
350
  "์•„๋ฉ”๋ฆฌ์นด"
351
  ]
352
 
353
+
354
+ @lru_cache(maxsize=100)
355
  def translate_query(query, country):
356
  try:
 
357
  if is_english(query):
 
358
  return query
359
 
 
360
  if country in COUNTRY_LANGUAGES:
 
361
  if country == "South Korea":
 
362
  return query
363
 
364
  target_lang = COUNTRY_LANGUAGES[country]
 
365
 
366
+ url = "https://translate.googleapis.com/translate_a/single"
367
  params = {
368
  "client": "gtx",
369
  "sl": "auto",
 
372
  "q": query
373
  }
374
 
375
+ session = requests.Session()
376
+ retries = Retry(total=3, backoff_factor=0.5)
377
+ session.mount('https://', HTTPAdapter(max_retries=retries))
378
+
379
+ response = session.get(url, params=params, timeout=(5, 10))
380
  translated_text = response.json()[0][0][0]
 
381
  return translated_text
382
 
383
  return query
 
386
  print(f"๋ฒˆ์—ญ ์˜ค๋ฅ˜: {str(e)}")
387
  return query
388
 
389
+
390
+ @lru_cache(maxsize=200)
391
  def translate_to_korean(text):
392
  try:
393
  url = "https://translate.googleapis.com/translate_a/single"
 
399
  "q": text
400
  }
401
 
402
+ session = requests.Session()
403
+ retries = Retry(total=3, backoff_factor=0.5)
404
+ session.mount('https://', HTTPAdapter(max_retries=retries))
405
+
406
+ response = session.get(url, params=params, timeout=(5, 10))
407
  translated_text = response.json()[0][0][0]
408
  return translated_text
409
  except Exception as e:
 
424
  date_range = f"{yesterday.strftime('%Y-%m-%d')},{now.strftime('%Y-%m-%d')}"
425
 
426
  translated_query = translate_query(query, country)
 
 
427
 
428
  payload = {
429
  "data": {
 
447
  }
448
 
449
  try:
450
+ session = requests.Session()
451
+ retries = Retry(total=3, backoff_factor=0.5)
452
+ session.mount('https://', HTTPAdapter(max_retries=retries))
453
 
454
+ response = session.post(url, json=payload, headers=headers, timeout=(5, 15))
455
  response.raise_for_status()
456
  return {"results": response.json(), "translated_query": translated_query}
457
  except requests.RequestException as e:
458
  return {"error": f"Error: {str(e)}", "translated_query": query}
459
 
460
+
461
+
462
  def format_results_from_raw(response_data):
463
  if "error" in response_data:
464
  return "Error: " + response_data["error"], []
 
492
  return format_results_from_raw(response_data)
493
 
494
 
495
+ def search_and_display(query, country, articles_state, progress=gr.Progress()):
496
+ with ThreadPoolExecutor(max_workers=3) as executor:
497
+ progress(0, desc="๊ฒ€์ƒ‰์–ด ๋ฒˆ์—ญ ์ค‘...")
498
+ future_translation = executor.submit(translate_query, query, country)
499
+ translated_query = future_translation.result()
500
+ translated_display = f"**์›๋ณธ ๊ฒ€์ƒ‰์–ด:** {query}\n**๋ฒˆ์—ญ๋œ ๊ฒ€์ƒ‰์–ด:** {translated_query}" if translated_query != query else f"**๊ฒ€์ƒ‰์–ด:** {query}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
 
502
+ progress(0.3, desc="๊ฒ€์ƒ‰ ์ค‘...")
503
+ response_data = search_serphouse(query, country)
 
 
504
 
505
+ progress(0.6, desc="๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ ์ค‘...")
506
+ error_message, articles = format_results_from_raw(response_data)
507
 
508
+ outputs = []
509
+ outputs.append(gr.update(value="๊ฒ€์ƒ‰์„ ์ง„ํ–‰์ค‘์ž…๋‹ˆ๋‹ค...", visible=True))
510
+ outputs.append(gr.update(value=translated_display, visible=True))
 
 
 
 
 
511
 
512
+ if error_message:
513
+ outputs.append(gr.update(value=error_message, visible=True))
514
+ for comp in article_components:
515
+ outputs.extend([
516
+ gr.update(visible=False), gr.update(), gr.update(),
517
+ gr.update(), gr.update()
518
+ ])
519
+ articles_state = []
520
  else:
521
+ outputs.append(gr.update(value="", visible=False))
522
+ if not error_message and articles:
523
+ futures = []
524
+ for article in articles:
525
+ future = executor.submit(translate_to_korean, article['snippet'])
526
+ futures.append((article, future))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
 
528
+ progress(0.8, desc="๋ฒˆ์—ญ ์ฒ˜๋ฆฌ ์ค‘...")
529
+ for article, future in futures:
530
+ article['korean_summary'] = future.result()
531
 
532
+ total_articles = len(articles)
533
+ for idx, comp in enumerate(article_components):
534
+ progress((idx + 1) / total_articles, desc=f"๊ฒฐ๊ณผ ํ‘œ์‹œ ์ค‘... {idx + 1}/{total_articles}")
535
+ if idx < len(articles):
536
+ article = articles[idx]
537
+ image_url = article['image_url']
538
+ 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)
539
+
540
  outputs.extend([
541
  gr.update(visible=True),
542
+ gr.update(value=f"### [{article['title']}]({article['link']})"),
543
+ image_update,
544
+ gr.update(value=f"**์š”์•ฝ:** {article['snippet']}\n\n**ํ•œ๊ธ€ ์š”์•ฝ:** {article['korean_summary']}"),
545
+ gr.update(value=f"**์ถœ์ฒ˜:** {article['channel']} | **์‹œ๊ฐ„:** {article['time']}")
 
 
 
 
 
 
 
546
  ])
547
  else:
548
  outputs.extend([
549
+ gr.update(visible=False), gr.update(), gr.update(),
550
+ gr.update(), gr.update()
 
 
 
 
551
  ])
552
+ articles_state = articles
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
553
 
554
+ progress(1.0, desc="์™„๋ฃŒ!")
555
+ outputs.append(articles_state)
556
+ outputs[0] = gr.update(value="", visible=False)
 
 
 
 
 
 
 
 
557
 
558
+ return outputs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
559
 
560
  def get_region_countries(region):
561
  """์„ ํƒ๋œ ์ง€์—ญ์˜ ๊ตญ๊ฐ€ ๋ฐ ์–ธ์–ด ์ •๋ณด ๋ฐ˜ํ™˜"""
 
661
  outputs[0] = gr.update(value=final_status, visible=True)
662
  yield outputs
663
 
664
+ css = """
665
+ /* ์ „์—ญ ์Šคํƒ€์ผ */
666
+ footer {visibility: hidden;}
667
 
668
+ /* ๋ ˆ์ด์•„์›ƒ ์ปจํ…Œ์ด๋„ˆ */
669
+ #status_area {
670
+ background: rgba(255, 255, 255, 0.9);
671
+ padding: 15px;
672
+ border-bottom: 1px solid #ddd;
673
+ margin-bottom: 20px;
674
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
675
+ }
676
 
677
+ #results_area {
678
+ padding: 10px;
679
+ margin-top: 10px;
680
+ }
681
 
682
+ /* ํƒญ ์Šคํƒ€์ผ */
683
+ .tabs {
684
+ border-bottom: 2px solid #ddd !important;
685
+ margin-bottom: 20px !important;
686
+ }
687
 
688
+ .tab-nav {
689
+ border-bottom: none !important;
690
+ margin-bottom: 0 !important;
691
+ }
692
 
693
+ .tab-nav button {
694
+ font-weight: bold !important;
695
+ padding: 10px 20px !important;
696
+ }
697
+
698
+ .tab-nav button.selected {
699
+ border-bottom: 2px solid #1f77b4 !important;
700
+ color: #1f77b4 !important;
701
+ }
702
+
703
+ /* ์ƒํƒœ ๋ฉ”์‹œ์ง€ */
704
+ #status_area .markdown-text {
705
+ font-size: 1.1em;
706
+ color: #2c3e50;
707
+ padding: 10px 0;
708
+ }
709
+
710
+ /* ๊ธฐ๋ณธ ์ปจํ…Œ์ด๋„ˆ */
711
+ .group {
712
+ border: 1px solid #eee;
713
+ padding: 15px;
714
+ margin-bottom: 15px;
715
+ border-radius: 5px;
716
+ background: white;
717
+ }
718
+
719
+ /* ๋ฒ„ํŠผ ์Šคํƒ€์ผ */
720
+ .primary-btn {
721
+ background: #1f77b4 !important;
722
+ border: none !important;
723
+ }
724
+
725
+ /* ์ž…๋ ฅ ํ•„๋“œ */
726
+ .textbox {
727
+ border: 1px solid #ddd !important;
728
+ border-radius: 4px !important;
729
+ }
730
+
731
+ /* ํ”„๋กœ๊ทธ๋ ˆ์Šค๋ฐ” ์ปจํ…Œ์ด๋„ˆ */
732
+ .progress-container {
733
+ position: fixed;
734
+ top: 0;
735
+ left: 0;
736
+ width: 100%;
737
+ height: 6px;
738
+ background: #e0e0e0;
739
+ z-index: 1000;
740
+ }
741
+
742
+ /* ํ”„๋กœ๊ทธ๋ ˆ์Šค๋ฐ” */
743
+ .progress-bar {
744
+ height: 100%;
745
+ background: linear-gradient(90deg, #2196F3, #00BCD4);
746
+ box-shadow: 0 0 10px rgba(33, 150, 243, 0.5);
747
+ transition: width 0.3s ease;
748
+ animation: progress-glow 1.5s ease-in-out infinite;
749
+ }
750
+
751
+ /* ํ”„๋กœ๊ทธ๋ ˆ์Šค ํ…์ŠคํŠธ */
752
+ .progress-text {
753
+ position: fixed;
754
+ top: 8px;
755
+ left: 50%;
756
+ transform: translateX(-50%);
757
+ background: #333;
758
+ color: white;
759
+ padding: 4px 12px;
760
+ border-radius: 15px;
761
+ font-size: 14px;
762
+ z-index: 1001;
763
+ box-shadow: 0 2px 5px rgba(0,0,0,0.2);
764
+ }
765
+
766
+ /* ํ”„๋กœ๊ทธ๋ ˆ์Šค๋ฐ” ์• ๋‹ˆ๋ฉ”์ด์…˜ */
767
+ @keyframes progress-glow {
768
+ 0% {
769
+ box-shadow: 0 0 5px rgba(33, 150, 243, 0.5);
770
+ }
771
+ 50% {
772
+ box-shadow: 0 0 20px rgba(33, 150, 243, 0.8);
773
+ }
774
+ 100% {
775
+ box-shadow: 0 0 5px rgba(33, 150, 243, 0.5);
776
+ }
777
+ }
778
+
779
+ /* ๋ฐ˜์‘ํ˜• ๋””์ž์ธ */
780
+ @media (max-width: 768px) {
781
+ .group {
782
+ padding: 10px;
783
+ margin-bottom: 15px;
784
+ }
785
+
786
+ .progress-text {
787
+ font-size: 12px;
788
+ padding: 3px 10px;
789
+ }
790
+ }
791
+
792
+ /* ๋กœ๋”ฉ ์ƒํƒœ ํ‘œ์‹œ ๊ฐœ์„  */
793
+ .loading {
794
+ opacity: 0.7;
795
+ pointer-events: none;
796
+ transition: opacity 0.3s ease;
797
+ }
798
+
799
+ /* ๊ฒฐ๊ณผ ์ปจํ…Œ์ด๋„ˆ ์• ๋‹ˆ๋ฉ”์ด์…˜ */
800
+ .group {
801
+ transition: all 0.3s ease;
802
+ opacity: 0;
803
+ transform: translateY(20px);
804
+ }
805
+
806
+ .group.visible {
807
+ opacity: 1;
808
+ transform: translateY(0);
809
+ }
810
+ """
811
 
812
  with gr.Blocks(theme="Nymbo/Nymbo_Theme", css=css, title="NewsAI ์„œ๋น„์Šค") as iface:
813
  with gr.Tabs():
 
886
  'index': i,
887
  })
888
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
889
  # ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ ๋ถ€๋ถ„
890
  # ๊ตญ๊ฐ€๋ณ„ ํƒญ ์ด๋ฒคํŠธ
891
  search_outputs = [status_message, translated_query_display, gr.Markdown(visible=False)]
 
919
  show_progress=True
920
  )
921
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
922
  iface.launch(
923
  server_name="0.0.0.0",
924
  server_port=7860,
 
926
  auth=("it1","chosun1"),
927
  ssl_verify=False,
928
  show_error=True
929
+ )