Spaces:
openfree
/
Running on CPU Upgrade

MoneyRadar / app.py
openfree's picture
Update app.py
bc3552e verified
raw
history blame
29.9 kB
import gradio as gr
import requests
import json
import os
from datetime import datetime, timedelta
from concurrent.futures import ThreadPoolExecutor
from functools import lru_cache
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
MAX_COUNTRY_RESULTS = 100 # ꡭ가별 μ΅œλŒ€ κ²°κ³Ό 수
MAX_GLOBAL_RESULTS = 1000 # 전세계 μ΅œλŒ€ κ²°κ³Ό 수
def create_article_components(max_results):
article_components = []
for i in range(max_results):
with gr.Group(visible=False) as article_group:
title = gr.Markdown()
image = gr.Image(width=200, height=150)
snippet = gr.Markdown()
info = gr.Markdown()
article_components.append({
'group': article_group,
'title': title,
'image': image,
'snippet': snippet,
'info': info,
'index': i,
})
return article_components
API_KEY = os.getenv("SERPHOUSE_API_KEY")
# ꡭ가별 μ–Έμ–΄ μ½”λ“œ 맀핑
COUNTRY_LANGUAGES = {
"United States": "en",
"United Kingdom": "en",
"Taiwan": "zh-TW",
"Canada": "en",
"Australia": "en",
"Germany": "de",
"France": "fr",
"Japan": "ja",
"China": "zh",
"India": "hi",
"Brazil": "pt",
"Mexico": "es",
"Russia": "ru",
"Italy": "it",
"Spain": "es",
"Netherlands": "nl",
"Singapore": "en",
"Hong Kong": "zh-HK",
"Indonesia": "id",
"Malaysia": "ms",
"Philippines": "tl",
"Thailand": "th",
"Vietnam": "vi",
"Belgium": "nl",
"Denmark": "da",
"Finland": "fi",
"Ireland": "en",
"Norway": "no",
"Poland": "pl",
"Sweden": "sv",
"Switzerland": "de",
"Austria": "de",
"Czech Republic": "cs",
"Greece": "el",
"Hungary": "hu",
"Portugal": "pt",
"Romania": "ro",
"Turkey": "tr",
"Israel": "he",
"Saudi Arabia": "ar",
"United Arab Emirates": "ar",
"South Africa": "en",
"Argentina": "es",
"Chile": "es",
"Colombia": "es",
"Peru": "es",
"Venezuela": "es",
"New Zealand": "en",
"Bangladesh": "bn",
"Pakistan": "ur",
"Egypt": "ar",
"Morocco": "ar",
"Nigeria": "en",
"Kenya": "sw",
"Ukraine": "uk",
"Croatia": "hr",
"Slovakia": "sk",
"Bulgaria": "bg",
"Serbia": "sr",
"Estonia": "et",
"Latvia": "lv",
"Lithuania": "lt",
"Slovenia": "sl",
"Luxembourg": "fr",
"Malta": "mt",
"Cyprus": "el",
"Iceland": "is"
}
COUNTRY_LOCATIONS = {
"United States": "United States",
"United Kingdom": "United Kingdom",
"Taiwan": "Taiwan",
"Canada": "Canada",
"Australia": "Australia",
"Germany": "Germany",
"France": "France",
"Japan": "Japan",
"China": "China",
"India": "India",
"Brazil": "Brazil",
"Mexico": "Mexico",
"Russia": "Russia",
"Italy": "Italy",
"Spain": "Spain",
"Netherlands": "Netherlands",
"Singapore": "Singapore",
"Hong Kong": "Hong Kong",
"Indonesia": "Indonesia",
"Malaysia": "Malaysia",
"Philippines": "Philippines",
"Thailand": "Thailand",
"Vietnam": "Vietnam",
"Belgium": "Belgium",
"Denmark": "Denmark",
"Finland": "Finland",
"Ireland": "Ireland",
"Norway": "Norway",
"Poland": "Poland",
"Sweden": "Sweden",
"Switzerland": "Switzerland",
"Austria": "Austria",
"Czech Republic": "Czech Republic",
"Greece": "Greece",
"Hungary": "Hungary",
"Portugal": "Portugal",
"Romania": "Romania",
"Turkey": "Turkey",
"Israel": "Israel",
"Saudi Arabia": "Saudi Arabia",
"United Arab Emirates": "United Arab Emirates",
"South Africa": "South Africa",
"Argentina": "Argentina",
"Chile": "Chile",
"Colombia": "Colombia",
"Peru": "Peru",
"Venezuela": "Venezuela",
"New Zealand": "New Zealand",
"Bangladesh": "Bangladesh",
"Pakistan": "Pakistan",
"Egypt": "Egypt",
"Morocco": "Morocco",
"Nigeria": "Nigeria",
"Kenya": "Kenya",
"Ukraine": "Ukraine",
"Croatia": "Croatia",
"Slovakia": "Slovakia",
"Bulgaria": "Bulgaria",
"Serbia": "Serbia",
"Estonia": "Estonia",
"Latvia": "Latvia",
"Lithuania": "Lithuania",
"Slovenia": "Slovenia",
"Luxembourg": "Luxembourg",
"Malta": "Malta",
"Cyprus": "Cyprus",
"Iceland": "Iceland"
}
# 지역 μ •μ˜
# λ™μ•„μ‹œμ•„ 지역
COUNTRY_LANGUAGES_EAST_ASIA = {
"Taiwan": "zh-TW",
"Japan": "ja",
"China": "zh",
"Hong Kong": "zh-HK"
}
COUNTRY_LOCATIONS_EAST_ASIA = {
"Taiwan": "Taiwan",
"Japan": "Japan",
"China": "China",
"Hong Kong": "Hong Kong"
}
# λ™λ‚¨μ•„μ‹œμ•„/μ˜€μ„Έμ•„λ‹ˆμ•„ 지역
COUNTRY_LANGUAGES_SOUTHEAST_ASIA_OCEANIA = {
"Indonesia": "id",
"Malaysia": "ms",
"Philippines": "tl",
"Thailand": "th",
"Vietnam": "vi",
"Singapore": "en",
"Papua New Guinea": "en",
"Australia": "en",
"New Zealand": "en"
}
COUNTRY_LOCATIONS_SOUTHEAST_ASIA_OCEANIA = {
"Indonesia": "Indonesia",
"Malaysia": "Malaysia",
"Philippines": "Philippines",
"Thailand": "Thailand",
"Vietnam": "Vietnam",
"Singapore": "Singapore",
"Papua New Guinea": "Papua New Guinea",
"Australia": "Australia",
"New Zealand": "New Zealand"
}
# λ™μœ λŸ½ 지역
COUNTRY_LANGUAGES_EAST_EUROPE = {
"Poland": "pl",
"Czech Republic": "cs",
"Greece": "el",
"Hungary": "hu",
"Romania": "ro",
"Ukraine": "uk",
"Croatia": "hr",
"Slovakia": "sk",
"Bulgaria": "bg",
"Serbia": "sr",
"Estonia": "et",
"Latvia": "lv",
"Lithuania": "lt",
"Slovenia": "sl",
"Malta": "mt",
"Cyprus": "el",
"Iceland": "is",
"Russia": "ru"
}
COUNTRY_LOCATIONS_EAST_EUROPE = {
"Poland": "Poland",
"Czech Republic": "Czech Republic",
"Greece": "Greece",
"Hungary": "Hungary",
"Romania": "Romania",
"Ukraine": "Ukraine",
"Croatia": "Croatia",
"Slovakia": "Slovakia",
"Bulgaria": "Bulgaria",
"Serbia": "Serbia",
"Estonia": "Estonia",
"Latvia": "Latvia",
"Lithuania": "Lithuania",
"Slovenia": "Slovenia",
"Malta": "Malta",
"Cyprus": "Cyprus",
"Iceland": "Iceland",
"Russia": "Russia"
}
# μ„œμœ λŸ½ 지역
COUNTRY_LANGUAGES_WEST_EUROPE = {
"Germany": "de",
"France": "fr",
"Italy": "it",
"Spain": "es",
"Netherlands": "nl",
"Belgium": "nl",
"Ireland": "en",
"Sweden": "sv",
"Switzerland": "de",
"Austria": "de",
"Portugal": "pt",
"Luxembourg": "fr",
"United Kingdom": "en"
}
COUNTRY_LOCATIONS_WEST_EUROPE = {
"Germany": "Germany",
"France": "France",
"Italy": "Italy",
"Spain": "Spain",
"Netherlands": "Netherlands",
"Belgium": "Belgium",
"Ireland": "Ireland",
"Sweden": "Sweden",
"Switzerland": "Switzerland",
"Austria": "Austria",
"Portugal": "Portugal",
"Luxembourg": "Luxembourg",
"United Kingdom": "United Kingdom"
}
# 쀑동/아프리카 지역
COUNTRY_LANGUAGES_ARAB_AFRICA = {
"South Africa": "en",
"Nigeria": "en",
"Kenya": "sw",
"Egypt": "ar",
"Morocco": "ar",
"Saudi Arabia": "ar",
"United Arab Emirates": "ar",
"Israel": "he"
}
COUNTRY_LOCATIONS_ARAB_AFRICA = {
"South Africa": "South Africa",
"Nigeria": "Nigeria",
"Kenya": "Kenya",
"Egypt": "Egypt",
"Morocco": "Morocco",
"Saudi Arabia": "Saudi Arabia",
"United Arab Emirates": "United Arab Emirates",
"Israel": "Israel"
}
# 아메리카 지역
COUNTRY_LANGUAGES_AMERICA = {
"United States": "en",
"Canada": "en",
"Mexico": "es",
"Brazil": "pt",
"Argentina": "es",
"Chile": "es",
"Colombia": "es",
"Peru": "es",
"Venezuela": "es"
}
COUNTRY_LOCATIONS_AMERICA = {
"United States": "United States",
"Canada": "Canada",
"Mexico": "Mexico",
"Brazil": "Brazil",
"Argentina": "Argentina",
"Chile": "Chile",
"Colombia": "Colombia",
"Peru": "Peru",
"Venezuela": "Venezuela"
}
# 지역 선택 리슀트
REGIONS = [
"λ™μ•„μ‹œμ•„",
"λ™λ‚¨μ•„μ‹œμ•„/μ˜€μ„Έμ•„λ‹ˆμ•„",
"λ™μœ λŸ½",
"μ„œμœ λŸ½",
"쀑동/아프리카",
"아메리카"
]
@lru_cache(maxsize=100)
def translate_query(query, country):
try:
if is_english(query):
return query
if country in COUNTRY_LANGUAGES:
if country == "South Korea":
return query
target_lang = COUNTRY_LANGUAGES[country]
url = "https://translate.googleapis.com/translate_a/single"
params = {
"client": "gtx",
"sl": "auto",
"tl": target_lang,
"dt": "t",
"q": query
}
session = requests.Session()
retries = Retry(total=3, backoff_factor=0.5)
session.mount('https://', HTTPAdapter(max_retries=retries))
response = session.get(url, params=params, timeout=(5, 10))
translated_text = response.json()[0][0][0]
return translated_text
return query
except Exception as e:
print(f"λ²ˆμ—­ 였λ₯˜: {str(e)}")
return query
@lru_cache(maxsize=200)
def translate_to_korean(text):
try:
url = "https://translate.googleapis.com/translate_a/single"
params = {
"client": "gtx",
"sl": "auto",
"tl": "ko",
"dt": "t",
"q": text
}
session = requests.Session()
retries = Retry(total=3, backoff_factor=0.5)
session.mount('https://', HTTPAdapter(max_retries=retries))
response = session.get(url, params=params, timeout=(5, 10))
translated_text = response.json()[0][0][0]
return translated_text
except Exception as e:
print(f"ν•œκΈ€ λ²ˆμ—­ 였λ₯˜: {str(e)}")
return text
def is_english(text):
return all(ord(char) < 128 for char in text.replace(' ', '').replace('-', '').replace('_', ''))
def is_korean(text):
return any('\uAC00' <= char <= '\uD7A3' for char in text)
def search_serphouse(query, country, page=1, num_result=10):
url = "https://api.serphouse.com/serp/live"
now = datetime.utcnow()
yesterday = now - timedelta(days=1)
date_range = f"{yesterday.strftime('%Y-%m-%d')},{now.strftime('%Y-%m-%d')}"
translated_query = translate_query(query, country)
payload = {
"data": {
"q": translated_query,
"domain": "google.com",
"loc": COUNTRY_LOCATIONS.get(country, "United States"),
"lang": COUNTRY_LANGUAGES.get(country, "en"),
"device": "desktop",
"serp_type": "news",
"page": "1",
"num": "100",
"date_range": date_range,
"sort_by": "date"
}
}
headers = {
"accept": "application/json",
"content-type": "application/json",
"authorization": f"Bearer {API_KEY}"
}
try:
session = requests.Session()
retries = Retry(total=3, backoff_factor=0.5)
session.mount('https://', HTTPAdapter(max_retries=retries))
response = session.post(url, json=payload, headers=headers, timeout=(5, 15))
response.raise_for_status()
return {"results": response.json(), "translated_query": translated_query}
except requests.RequestException as e:
return {"error": f"Error: {str(e)}", "translated_query": query}
def format_results_from_raw(response_data):
if "error" in response_data:
return "Error: " + response_data["error"], []
try:
results = response_data["results"]
translated_query = response_data["translated_query"]
news_results = results.get('results', {}).get('results', {}).get('news', [])
if not news_results:
return "검색 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€.", []
# ν•œκ΅­ 도메인 및 ν•œκ΅­ κ΄€λ ¨ ν‚€μ›Œλ“œ 필터링
korean_domains = ['.kr', 'korea', 'korean', 'yonhap', 'hankyung', 'chosun',
'donga', 'joins', 'hani', 'koreatimes', 'koreaherald']
korean_keywords = ['korea', 'korean', 'seoul', 'busan', 'incheon', 'daegu',
'gwangju', 'daejeon', 'ulsan', 'sejong']
filtered_articles = []
for idx, result in enumerate(news_results, 1):
url = result.get("url", result.get("link", "")).lower()
title = result.get("title", "").lower()
channel = result.get("channel", result.get("source", "")).lower()
# ν•œκ΅­ κ΄€λ ¨ 컨텐츠 필터링
is_korean_content = any(domain in url or domain in channel for domain in korean_domains) or \
any(keyword in title.lower() for keyword in korean_keywords)
if not is_korean_content:
filtered_articles.append({
"index": idx,
"title": result.get("title", "제λͺ© μ—†μŒ"),
"link": url,
"snippet": result.get("snippet", "λ‚΄μš© μ—†μŒ"),
"channel": result.get("channel", result.get("source", "μ•Œ 수 μ—†μŒ")),
"time": result.get("time", result.get("date", "μ•Œ 수 μ—†λŠ” μ‹œκ°„")),
"image_url": result.get("img", result.get("thumbnail", "")),
"translated_query": translated_query
})
return "", filtered_articles
except Exception as e:
return f"κ²°κ³Ό 처리 쀑 였λ₯˜ λ°œμƒ: {str(e)}", []
def serphouse_search(query, country):
response_data = search_serphouse(query, country)
return format_results_from_raw(response_data)
def search_and_display(query, country, articles_state, progress=gr.Progress()):
with ThreadPoolExecutor(max_workers=3) as executor:
progress(0, desc="검색어 λ²ˆμ—­ 쀑...")
future_translation = executor.submit(translate_query, query, country)
translated_query = future_translation.result()
translated_display = f"**원본 검색어:** {query}\n**λ²ˆμ—­λœ 검색어:** {translated_query}" if translated_query != query else f"**검색어:** {query}"
progress(0.3, desc="검색 쀑...")
response_data = search_serphouse(query, country)
progress(0.6, desc="κ²°κ³Ό 처리 쀑...")
error_message, articles = format_results_from_raw(response_data)
outputs = []
outputs.append(gr.update(value="검색을 μ§„ν–‰μ€‘μž…λ‹ˆλ‹€...", visible=True))
outputs.append(gr.update(value=translated_display, visible=True))
if error_message:
outputs.append(gr.update(value=error_message, visible=True))
for comp in article_components:
outputs.extend([
gr.update(visible=False), gr.update(), gr.update(),
gr.update(), gr.update()
])
articles_state = []
else:
outputs.append(gr.update(value="", visible=False))
if not error_message and articles:
futures = []
for article in articles:
future = executor.submit(translate_to_korean, article['snippet'])
futures.append((article, future))
progress(0.8, desc="λ²ˆμ—­ 처리 쀑...")
for article, future in futures:
article['korean_summary'] = future.result()
total_articles = len(articles)
for idx, comp in enumerate(article_components):
progress((idx + 1) / total_articles, desc=f"κ²°κ³Ό ν‘œμ‹œ 쀑... {idx + 1}/{total_articles}")
if idx < len(articles):
article = articles[idx]
image_url = article['image_url']
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)
outputs.extend([
gr.update(visible=True),
gr.update(value=f"### [{article['title']}]({article['link']})"),
image_update,
gr.update(value=f"**μš”μ•½:** {article['snippet']}\n\n**ν•œκΈ€ μš”μ•½:** {article['korean_summary']}"),
gr.update(value=f"**좜처:** {article['channel']} | **μ‹œκ°„:** {article['time']}")
])
else:
outputs.extend([
gr.update(visible=False), gr.update(), gr.update(),
gr.update(), gr.update()
])
articles_state = articles
progress(1.0, desc="μ™„λ£Œ!")
outputs.append(articles_state)
outputs[0] = gr.update(value="", visible=False)
return outputs
def get_region_countries(region):
"""μ„ νƒλœ μ§€μ—­μ˜ κ΅­κ°€ 및 μ–Έμ–΄ 정보 λ°˜ν™˜"""
if region == "λ™μ•„μ‹œμ•„":
return COUNTRY_LOCATIONS_EAST_ASIA, COUNTRY_LANGUAGES_EAST_ASIA
elif region == "λ™λ‚¨μ•„μ‹œμ•„/μ˜€μ„Έμ•„λ‹ˆμ•„":
return COUNTRY_LOCATIONS_SOUTHEAST_ASIA_OCEANIA, COUNTRY_LANGUAGES_SOUTHEAST_ASIA_OCEANIA
elif region == "λ™μœ λŸ½":
return COUNTRY_LOCATIONS_EAST_EUROPE, COUNTRY_LANGUAGES_EAST_EUROPE
elif region == "μ„œμœ λŸ½":
return COUNTRY_LOCATIONS_WEST_EUROPE, COUNTRY_LANGUAGES_WEST_EUROPE
elif region == "쀑동/아프리카":
return COUNTRY_LOCATIONS_ARAB_AFRICA, COUNTRY_LANGUAGES_ARAB_AFRICA
elif region == "아메리카":
return COUNTRY_LOCATIONS_AMERICA, COUNTRY_LANGUAGES_AMERICA
return {}, {}
def search_global(query, region, articles_state_global):
"""지역별 검색 ν•¨μˆ˜"""
status_msg = f"{region} 지역 검색을 μ‹œμž‘ν•©λ‹ˆλ‹€..."
all_results = []
outputs = [
gr.update(value=status_msg, visible=True),
gr.update(value=f"**검색어:** {query}", visible=True),
]
for _ in global_article_components:
outputs.extend([
gr.update(visible=False), gr.update(), gr.update(),
gr.update(), gr.update()
])
outputs.append([])
yield outputs
# μ„ νƒλœ μ§€μ—­μ˜ κ΅­κ°€ 정보 κ°€μ Έμ˜€κΈ°
locations, languages = get_region_countries(region)
total_countries = len(locations)
for idx, (country, location) in enumerate(locations.items(), 1):
try:
status_msg = f"{region} - {country} 검색 쀑... ({idx}/{total_countries} κ΅­κ°€)"
outputs[0] = gr.update(value=status_msg, visible=True)
yield outputs
error_message, articles = serphouse_search(query, country)
if not error_message and articles:
for article in articles:
article['source_country'] = country
article['region'] = region
all_results.extend(articles)
sorted_results = sorted(all_results, key=lambda x: x.get('time', ''), reverse=True)
seen_urls = set()
unique_results = []
for article in sorted_results:
url = article.get('link', '')
if url not in seen_urls:
seen_urls.add(url)
unique_results.append(article)
unique_results = unique_results[:MAX_GLOBAL_RESULTS]
outputs = [
gr.update(value=f"{region} - {idx}/{total_countries} κ΅­κ°€ 검색 μ™„λ£Œ\nν˜„μž¬κΉŒμ§€ 발견된 λ‰΄μŠ€: {len(unique_results)}건", visible=True),
gr.update(value=f"**검색어:** {query} | **지역:** {region}", visible=True),
]
for idx, comp in enumerate(global_article_components):
if idx < len(unique_results):
article = unique_results[idx]
image_url = article.get('image_url', '')
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)
korean_summary = translate_to_korean(article['snippet'])
outputs.extend([
gr.update(visible=True),
gr.update(value=f"### [{article['title']}]({article['link']})"),
image_update,
gr.update(value=f"**μš”μ•½:** {article['snippet']}\n\n**ν•œκΈ€ μš”μ•½:** {korean_summary}"),
gr.update(value=f"**좜처:** {article['channel']} | **κ΅­κ°€:** {article['source_country']} | **지역:** {article['region']} | **μ‹œκ°„:** {article['time']}")
])
else:
outputs.extend([
gr.update(visible=False),
gr.update(),
gr.update(),
gr.update(),
gr.update()
])
outputs.append(unique_results)
yield outputs
except Exception as e:
print(f"Error searching {country}: {str(e)}")
continue
final_status = f"{region} 검색 μ™„λ£Œ! 총 {len(unique_results)}개의 λ‰΄μŠ€κ°€ λ°œκ²¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€."
outputs[0] = gr.update(value=final_status, visible=True)
yield outputs
css = """
/* μ „μ—­ μŠ€νƒ€μΌ */
footer {visibility: hidden;}
/* λ ˆμ΄μ•„μ›ƒ μ»¨ν…Œμ΄λ„ˆ */
#status_area {
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-bottom: 1px solid #ddd;
margin-bottom: 20px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
#results_area {
padding: 10px;
margin-top: 10px;
}
/* νƒ­ μŠ€νƒ€μΌ */
.tabs {
border-bottom: 2px solid #ddd !important;
margin-bottom: 20px !important;
}
.tab-nav {
border-bottom: none !important;
margin-bottom: 0 !important;
}
.tab-nav button {
font-weight: bold !important;
padding: 10px 20px !important;
}
.tab-nav button.selected {
border-bottom: 2px solid #1f77b4 !important;
color: #1f77b4 !important;
}
/* μƒνƒœ λ©”μ‹œμ§€ */
#status_area .markdown-text {
font-size: 1.1em;
color: #2c3e50;
padding: 10px 0;
}
/* κΈ°λ³Έ μ»¨ν…Œμ΄λ„ˆ */
.group {
border: 1px solid #eee;
padding: 15px;
margin-bottom: 15px;
border-radius: 5px;
background: white;
}
/* λ²„νŠΌ μŠ€νƒ€μΌ */
.primary-btn {
background: #1f77b4 !important;
border: none !important;
}
/* μž…λ ₯ ν•„λ“œ */
.textbox {
border: 1px solid #ddd !important;
border-radius: 4px !important;
}
/* ν”„λ‘œκ·Έλ ˆμŠ€λ°” μ»¨ν…Œμ΄λ„ˆ */
.progress-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 6px;
background: #e0e0e0;
z-index: 1000;
}
/* ν”„λ‘œκ·Έλ ˆμŠ€λ°” */
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #2196F3, #00BCD4);
box-shadow: 0 0 10px rgba(33, 150, 243, 0.5);
transition: width 0.3s ease;
animation: progress-glow 1.5s ease-in-out infinite;
}
/* ν”„λ‘œκ·Έλ ˆμŠ€ ν…μŠ€νŠΈ */
.progress-text {
position: fixed;
top: 8px;
left: 50%;
transform: translateX(-50%);
background: #333;
color: white;
padding: 4px 12px;
border-radius: 15px;
font-size: 14px;
z-index: 1001;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
/* ν”„λ‘œκ·Έλ ˆμŠ€λ°” μ• λ‹ˆλ©”μ΄μ…˜ */
@keyframes progress-glow {
0% {
box-shadow: 0 0 5px rgba(33, 150, 243, 0.5);
}
50% {
box-shadow: 0 0 20px rgba(33, 150, 243, 0.8);
}
100% {
box-shadow: 0 0 5px rgba(33, 150, 243, 0.5);
}
}
/* λ°˜μ‘ν˜• λ””μžμΈ */
@media (max-width: 768px) {
.group {
padding: 10px;
margin-bottom: 15px;
}
.progress-text {
font-size: 12px;
padding: 3px 10px;
}
}
/* λ‘œλ”© μƒνƒœ ν‘œμ‹œ κ°œμ„  */
.loading {
opacity: 0.7;
pointer-events: none;
transition: opacity 0.3s ease;
}
/* κ²°κ³Ό μ»¨ν…Œμ΄λ„ˆ μ• λ‹ˆλ©”μ΄μ…˜ */
.group {
transition: all 0.3s ease;
opacity: 0;
transform: translateY(20px);
}
.group.visible {
opacity: 1;
transform: translateY(0);
}
"""
with gr.Blocks(theme="Nymbo/Nymbo_Theme", css=css, title="NewsAI μ„œλΉ„μŠ€") as iface:
with gr.Tabs():
# ꡭ가별 νƒ­
with gr.Tab("ꡭ가별"):
gr.Markdown("검색어λ₯Ό μž…λ ₯ν•˜κ³  μ›ν•˜λŠ” κ΅­κ°€(ν•œκ΅­ μ œμ™Έ)λ₯Όλ₯Ό μ„ νƒν•˜λ©΄, 검색어와 μΌμΉ˜ν•˜λŠ” 24μ‹œκ°„ 이내 λ‰΄μŠ€λ₯Ό μ΅œλŒ€ 100개 좜λ ₯ν•©λ‹ˆλ‹€.")
gr.Markdown("κ΅­κ°€ 선택후 검색어에 'ν•œκΈ€'을 μž…λ ₯ν•˜λ©΄ ν˜„μ§€ μ–Έμ–΄λ‘œ λ²ˆμ—­λ˜μ–΄ κ²€μƒ‰ν•©λ‹ˆλ‹€. 예: 'Taiwan' κ΅­κ°€ 선택후 'μ‚Όμ„±' μž…λ ₯μ‹œ 'δΈ‰ζ˜Ÿ'으둜 μžλ™ 검색")
with gr.Column():
with gr.Row():
query = gr.Textbox(label="검색어")
country = gr.Dropdown(
choices=sorted(list(COUNTRY_LOCATIONS.keys())),
label="κ΅­κ°€",
value="United States"
)
status_message = gr.Markdown("", visible=True)
translated_query_display = gr.Markdown(visible=False)
search_button = gr.Button("검색", variant="primary")
progress = gr.Progress()
articles_state = gr.State([])
article_components = []
for i in range(100):
with gr.Group(visible=False) as article_group:
title = gr.Markdown()
image = gr.Image(width=200, height=150)
snippet = gr.Markdown()
info = gr.Markdown()
article_components.append({
'group': article_group,
'title': title,
'image': image,
'snippet': snippet,
'info': info,
'index': i,
})
# 전세계 νƒ­
with gr.Tab("전세계"):
gr.Markdown("λŒ€λ₯™λ³„λ‘œ 24μ‹œκ°„ 이내 λ‰΄μŠ€λ₯Ό κ²€μƒ‰ν•©λ‹ˆλ‹€.")
with gr.Column():
with gr.Column(elem_id="status_area"):
with gr.Row():
query_global = gr.Textbox(label="검색어")
region_select = gr.Dropdown(
choices=REGIONS,
label="지역 선택",
value="λ™μ•„μ‹œμ•„"
)
search_button_global = gr.Button("검색", variant="primary")
status_message_global = gr.Markdown("")
translated_query_display_global = gr.Markdown("")
with gr.Column(elem_id="results_area"):
articles_state_global = gr.State([])
global_article_components = []
for i in range(MAX_GLOBAL_RESULTS):
with gr.Group(visible=False) as article_group:
title = gr.Markdown()
image = gr.Image(width=200, height=150)
snippet = gr.Markdown()
info = gr.Markdown()
global_article_components.append({
'group': article_group,
'title': title,
'image': image,
'snippet': snippet,
'info': info,
'index': i,
})
# 이벀트 μ—°κ²° λΆ€λΆ„
# ꡭ가별 νƒ­ 이벀트
search_outputs = [status_message, translated_query_display, gr.Markdown(visible=False)]
for comp in article_components:
search_outputs.extend([
comp['group'], comp['title'], comp['image'],
comp['snippet'], comp['info']
])
search_outputs.append(articles_state)
search_button.click(
fn=search_and_display,
inputs=[query, country, articles_state],
outputs=search_outputs,
show_progress=True
)
# 전세계 νƒ­ 이벀트
global_search_outputs = [status_message_global, translated_query_display_global]
for comp in global_article_components:
global_search_outputs.extend([
comp['group'], comp['title'], comp['image'],
comp['snippet'], comp['info']
])
global_search_outputs.append(articles_state_global)
search_button_global.click(
fn=search_global,
inputs=[query_global, region_select, articles_state_global],
outputs=global_search_outputs,
show_progress=True
)
iface.launch(
server_name="0.0.0.0",
server_port=7860,
share=True,
auth=("it1","chosun1"),
ssl_verify=False,
show_error=True
)