openfree's picture
Update app.py
6ba1af9 verified
raw
history blame
33.8 kB
import gradio as gr
import requests
import json
import os
from datetime import datetime, timedelta
from concurrent.futures import ThreadPoolExecutor, as_completed
from functools import lru_cache
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
from openai import OpenAI
from bs4 import BeautifulSoup
import re
import pathlib
import sqlite3
import pytz
# List of target companies/keywords (์˜๋ฌธ์œผ๋กœ ๊ฒ€์ƒ‰์–ด๋Š” ๊ทธ๋Œ€๋กœ ์œ ์ง€)
KOREAN_COMPANIES = [
"NVIDIA",
"ALPHABET",
"APPLE",
"TESLA",
"AMAZON",
"MICROSOFT",
"META",
"INTEL",
"SAMSUNG",
"HYNIX",
"BITCOIN",
"crypto",
"stock",
"Economics",
"Finance",
"investing"
]
def convert_to_seoul_time(timestamp_str):
"""
์ฃผ์–ด์ง„ UTC ํƒ€์ž„์Šคํƒฌํ”„ ๋ฌธ์ž์—ด์„ ์„œ์šธ ์‹œ๊ฐ„(KST)์œผ๋กœ ๋ณ€ํ™˜.
"""
try:
dt = datetime.strptime(timestamp_str, '%Y-%m-%d %H:%M:%S')
seoul_tz = pytz.timezone('Asia/Seoul')
seoul_time = seoul_tz.localize(dt)
return seoul_time.strftime('%Y-%m-%d %H:%M:%S KST')
except Exception as e:
print(f"์‹œ๊ฐ„ ๋ณ€ํ™˜ ์˜ค๋ฅ˜: {str(e)}")
return timestamp_str
def analyze_sentiment_batch(articles, client):
"""
OpenAI API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋‰ด์Šค ๊ธฐ์‚ฌ ๋ชจ์Œ์— ๋Œ€ํ•œ ์ „๋ฐ˜์ ์ธ ๊ฐ์„ฑ ๋ถ„์„์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.
๋ถ„์„ ๊ฒฐ๊ณผ๋Š” ํ•œ๊ธ€๋กœ ๋ฐ˜ํ™˜๋œ๋‹ค.
"""
try:
# ๋ชจ๋“  ๊ธฐ์‚ฌ๋ฅผ ํ•˜๋‚˜์˜ ํ…์ŠคํŠธ๋กœ ๊ฒฐํ•ฉ
combined_text = "\n\n".join([
f"์ œ๋ชฉ: {article.get('title', '')}\n๋‚ด์šฉ: {article.get('snippet', '')}"
for article in articles
])
prompt = f"""์•„๋ž˜ ๋‰ด์Šค ๊ธฐ์‚ฌ ๋ชจ์Œ์— ๋Œ€ํ•œ ์ „๋ฐ˜์ ์ธ ๊ฐ์„ฑ ๋ถ„์„์„ ์ˆ˜ํ–‰ํ•˜์„ธ์š”.
๋‰ด์Šค ๋‚ด์šฉ:
{combined_text}
๋‹ค์Œ ํ˜•์‹์„ ๋”ฐ๋ผ ์ž‘์„ฑํ•ด ์ฃผ์„ธ์š”:
1. ์ „์ฒด ๊ฐ์„ฑ: [๊ธ์ •/๋ถ€์ •/์ค‘๋ฆฝ]
2. ์ฃผ์š” ๊ธ์ • ์š”์ธ:
- [ํ•ญ๋ชฉ1]
- [ํ•ญ๋ชฉ2]
3. ์ฃผ์š” ๋ถ€์ • ์š”์ธ:
- [ํ•ญ๋ชฉ1]
- [ํ•ญ๋ชฉ2]
4. ์š”์•ฝ: [์ž์„ธํ•œ ์„ค๋ช…]
"""
response = client.chat.completions.create(
model="CohereForAI/c4ai-command-r-plus-08-2024",
messages=[{"role": "user", "content": prompt}],
temperature=0.3,
max_tokens=1000
)
return response.choices[0].message.content
except Exception as e:
return f"๊ฐ์„ฑ ๋ถ„์„ ์‹คํŒจ: {str(e)}"
# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™”
def init_db():
"""
SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค(search_results.db)๋ฅผ ์ดˆ๊ธฐํ™” (์—†์œผ๋ฉด ์ƒ์„ฑ).
"""
db_path = pathlib.Path("search_results.db")
conn = sqlite3.connect(db_path)
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS searches
(id INTEGER PRIMARY KEY AUTOINCREMENT,
keyword TEXT,
country TEXT,
results TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP)''')
conn.commit()
conn.close()
def save_to_db(keyword, country, results):
"""
ํŠน์ • (ํ‚ค์›Œ๋“œ, ๊ตญ๊ฐ€) ์กฐํ•ฉ์— ๋Œ€ํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ.
"""
conn = sqlite3.connect("search_results.db")
c = conn.cursor()
seoul_tz = pytz.timezone('Asia/Seoul')
now = datetime.now(seoul_tz)
timestamp = now.strftime('%Y-%m-%d %H:%M:%S')
c.execute("""INSERT INTO searches
(keyword, country, results, timestamp)
VALUES (?, ?, ?, ?)""",
(keyword, country, json.dumps(results), timestamp))
conn.commit()
conn.close()
def load_from_db(keyword, country):
"""
ํŠน์ • (ํ‚ค์›Œ๋“œ, ๊ตญ๊ฐ€) ์กฐํ•ฉ์— ๋Œ€ํ•œ ๊ฐ€์žฅ ์ตœ๊ทผ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๋ถˆ๋Ÿฌ์˜ด.
๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ณ  ํƒ€์ž„์Šคํƒฌํ”„๋ฅผ ๋ณ€ํ™˜ํ•˜์—ฌ ๋ฐ˜ํ™˜.
"""
conn = sqlite3.connect("search_results.db")
c = conn.cursor()
c.execute(
"SELECT results, timestamp FROM searches WHERE keyword=? AND country=? ORDER BY timestamp DESC LIMIT 1",
(keyword, country)
)
result = c.fetchone()
conn.close()
if result:
return json.loads(result[0]), convert_to_seoul_time(result[1])
return None, None
def display_results(articles):
"""
๋‰ด์Šค ๊ธฐ์‚ฌ ๋ชฉ๋ก์„ ๋งˆํฌ๋‹ค์šด ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๋ฐ˜ํ™˜.
"""
output = ""
for idx, article in enumerate(articles, 1):
output += f"### {idx}. {article['title']}\n"
output += f"์ถœ์ฒ˜: {article['channel']}\n"
output += f"์‹œ๊ฐ„: {article['time']}\n"
output += f"๋งํฌ: {article['link']}\n"
output += f"์š”์•ฝ: {article['snippet']}\n\n"
return output
########################################
# 1) ๊ฒ€์ƒ‰ => ๊ธฐ์‚ฌ + ๊ฐ์„ฑ ๋ถ„์„, DB ์ €์žฅ
########################################
def search_company(company):
"""
๋‹จ์ผ ํšŒ์‚ฌ(๋˜๋Š” ํ‚ค์›Œ๋“œ)์— ๋Œ€ํ•ด ๋ฏธ๊ตญ ๋‰ด์Šค ๊ฒ€์ƒ‰์„ ์ง„ํ–‰:
1) ๊ธฐ์‚ฌ ๋ชฉ๋ก์„ ๊ฒ€์ƒ‰
2) ๊ฐ์„ฑ ๋ถ„์„ ์ˆ˜ํ–‰
3) ๊ฒฐ๊ณผ๋ฅผ DB์— ์ €์žฅ
4) ๊ธฐ์‚ฌ์™€ ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ํ•˜๋‚˜์˜ ์ถœ๋ ฅ์œผ๋กœ ๋ฐ˜ํ™˜
"""
error_message, articles = serphouse_search(company, "United States")
if not error_message and articles:
# ๊ฐ์„ฑ ๋ถ„์„ ์ˆ˜ํ–‰ (ํ•œ๊ธ€ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜)
analysis = analyze_sentiment_batch(articles, client)
# DB์— ์ €์žฅํ•  ๋ฐ์ดํ„ฐ ์ค€๋น„
store_dict = {
"articles": articles,
"analysis": analysis
}
save_to_db(company, "United States", store_dict)
# ์ถœ๋ ฅ์šฉ ๋ฐ์ดํ„ฐ ์ค€๋น„
output = display_results(articles)
output += f"\n\n### ๊ฐ์„ฑ ๋ถ„์„ ๋ณด๊ณ ์„œ\n{analysis}\n"
return output
return f"{company}์— ๋Œ€ํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."
########################################
# 2) ๋ถˆ๋Ÿฌ์˜ค๊ธฐ => DB์—์„œ ๊ธฐ์‚ฌ + ๋ถ„์„ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜
########################################
def load_company(company):
"""
์ฃผ์–ด์ง„ ํšŒ์‚ฌ(๋˜๋Š” ํ‚ค์›Œ๋“œ)์— ๋Œ€ํ•ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๊ฐ€์žฅ ์ตœ๊ทผ ๋ฏธ๊ตญ ๋‰ด์Šค ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ(๊ธฐ์‚ฌ + ๊ฐ์„ฑ ๋ถ„์„)๋ฅผ ๋ถˆ๋Ÿฌ์˜ด.
"""
data, timestamp = load_from_db(company, "United States")
if data:
articles = data.get("articles", [])
analysis = data.get("analysis", "")
output = f"### {company} ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ\n๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ: {timestamp}\n\n"
output += display_results(articles)
output += f"\n\n### ๊ฐ์„ฑ ๋ถ„์„ ๋ณด๊ณ ์„œ\n{analysis}\n"
return output
return f"{company}์— ๋Œ€ํ•œ ์ €์žฅ๋œ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."
########################################
# 3) show_stats() โ€“ ๋ณด๊ณ ์„œ ์ œ๋ชฉ ๋ณ€๊ฒฝ
########################################
def show_stats():
"""
๊ฐ ํšŒ์‚ฌ(KOREAN_COMPANIES)์— ๋Œ€ํ•ด:
- DB์— ์ €์žฅ๋œ ์ตœ์‹  ํƒ€์ž„์Šคํƒฌํ”„
- ์ €์žฅ๋œ ๊ธฐ์‚ฌ ๊ฐœ์ˆ˜
- ๊ฐ์„ฑ ๋ถ„์„ ๊ฒฐ๊ณผ
์„ ๋ณด๊ณ ์„œ ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜.
์ œ๋ชฉ: "EarnBOT ๋ถ„์„ ๋ณด๊ณ ์„œ"
"""
conn = sqlite3.connect("search_results.db")
c = conn.cursor()
output = "## EarnBOT ๋ถ„์„ ๋ณด๊ณ ์„œ\n\n"
data_list = []
for company in KOREAN_COMPANIES:
c.execute("""
SELECT results, timestamp
FROM searches
WHERE keyword = ?
ORDER BY timestamp DESC
LIMIT 1
""", (company,))
row = c.fetchone()
if row:
results_json, timestamp = row
data_list.append((company, timestamp, results_json))
conn.close()
def analyze_data(item):
comp, tstamp, results_json = item
data = json.loads(results_json)
articles = data.get("articles", [])
analysis = data.get("analysis", "")
count_articles = len(articles)
return (comp, tstamp, count_articles, analysis)
results_list = []
with ThreadPoolExecutor(max_workers=5) as executor:
futures = [executor.submit(analyze_data, dl) for dl in data_list]
for future in as_completed(futures):
results_list.append(future.result())
for comp, tstamp, count, analysis in results_list:
seoul_time = convert_to_seoul_time(tstamp)
output += f"### {comp}\n"
output += f"- ๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ: {seoul_time}\n"
output += f"- ์ €์žฅ๋œ ๊ธฐ์‚ฌ ์ˆ˜: {count}\n\n"
if analysis:
output += "#### ๋‰ด์Šค ๊ฐ์„ฑ ๋ถ„์„\n"
output += f"{analysis}\n\n"
output += "---\n\n"
return output
def search_all_companies():
"""
KOREAN_COMPANIES์˜ ๋ชจ๋“  ํ•ญ๋ชฉ์— ๋Œ€ํ•ด(๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ) ๊ฒ€์ƒ‰ ํ›„,
๊ฐ์„ฑ ๋ถ„์„ ์ˆ˜ํ–‰ ๋ฐ DB ์ €์žฅ => ๋ชจ๋“  ๊ฒฐ๊ณผ๋ฅผ ๋งˆํฌ๋‹ค์šด ๋ฌธ์ž์—ด๋กœ ๋ฐ˜ํ™˜.
"""
overall_result = "# [์ „์ฒด ํšŒ์‚ฌ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ]\n\n"
def do_search(comp):
return comp, search_company(comp)
with ThreadPoolExecutor(max_workers=5) as executor:
futures = [executor.submit(do_search, c) for c in KOREAN_COMPANIES]
for future in as_completed(futures):
comp, res_text = future.result()
overall_result += f"## {comp}\n"
overall_result += res_text + "\n\n"
return overall_result
def load_all_companies():
"""
DB์— ์ €์žฅ๋œ ๋ชจ๋“  ํšŒ์‚ฌ(KOREAN_COMPANIES)์˜ ๊ธฐ์‚ฌ์™€ ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ๋ถˆ๋Ÿฌ์™€ ๋งˆํฌ๋‹ค์šด์œผ๋กœ ๋ฐ˜ํ™˜.
"""
overall_result = "# [์ „์ฒด ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ ์ถœ๋ ฅ]\n\n"
for comp in KOREAN_COMPANIES:
overall_result += f"## {comp}\n"
overall_result += load_company(comp)
overall_result += "\n"
return overall_result
def full_summary_report():
"""
1) ๋ชจ๋“  ํšŒ์‚ฌ๋ฅผ ๊ฒ€์ƒ‰(๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ) -> 2) DB์—์„œ ๊ฒฐ๊ณผ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ -> 3) ๊ฐ์„ฑ ๋ถ„์„ ํ†ต๊ณ„ ํ‘œ์‹œ
์„ธ ๋‹จ๊ณ„์˜ ๊ฒฐ๊ณผ๋ฅผ ํ•˜๋‚˜์˜ ์ข…ํ•ฉ ๋ณด๊ณ ์„œ๋กœ ๋ฐ˜ํ™˜.
"""
# 1) ๋ชจ๋“  ํšŒ์‚ฌ ๊ฒ€์ƒ‰ ๋ฐ DB ์ €์žฅ
search_result_text = search_all_companies()
# 2) DB์—์„œ ๊ฒฐ๊ณผ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
load_result_text = load_all_companies()
# 3) ํ†ต๊ณ„ ํ‘œ์‹œ โ€“ EarnBOT ๋ถ„์„ ๋ณด๊ณ ์„œ
stats_text = show_stats()
combined_report = (
"# ์ „์ฒด ๋ถ„์„ ์ข…ํ•ฉ ๋ณด๊ณ ์„œ\n\n"
"์‹คํ–‰ ์ˆœ์„œ:\n"
"1. ๋ชจ๋“  ํšŒ์‚ฌ ๊ฒ€์ƒ‰(๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ) ๋ฐ ๊ฐ์„ฑ ๋ถ„์„ โ†’ 2. DB์—์„œ ๊ฒฐ๊ณผ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ โ†’ 3. ์ „์ฒด ๊ฐ์„ฑ ๋ถ„์„ ํ†ต๊ณ„ ํ‘œ์‹œ\n\n"
f"{search_result_text}\n\n"
f"{load_result_text}\n\n"
"## [์ „์ฒด ๊ฐ์„ฑ ๋ถ„์„ ํ†ต๊ณ„]\n\n"
f"{stats_text}"
)
return combined_report
########################################
# ์ถ”๊ฐ€ ๊ธฐ๋Šฅ: ์‚ฌ์šฉ์ž ์ •์˜ ๊ฒ€์ƒ‰
########################################
def search_custom(query, country):
"""
์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ (ํ‚ค์›Œ๋“œ, ๊ตญ๊ฐ€)์— ๋Œ€ํ•ด:
1) ๊ฒ€์ƒ‰ ๋ฐ ๊ฐ์„ฑ ๋ถ„์„ ์ˆ˜ํ–‰ ํ›„ DB ์ €์žฅ
2) DB์—์„œ ๋ถˆ๋Ÿฌ์™€ ๊ธฐ์‚ฌ์™€ ๋ถ„์„ ๊ฒฐ๊ณผ ํ‘œ์‹œ
"""
error_message, articles = serphouse_search(query, country)
if error_message:
return f"์˜ค๋ฅ˜ ๋ฐœ์ƒ: {error_message}"
if not articles:
return "์ž…๋ ฅํ•˜์‹  ๊ฒ€์ƒ‰์–ด์— ๋Œ€ํ•œ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."
# 1) ๊ฐ์„ฑ ๋ถ„์„ ์ˆ˜ํ–‰ (ํ•œ๊ธ€)
analysis = analyze_sentiment_batch(articles, client)
# 2) DB์— ์ €์žฅ
save_data = {
"articles": articles,
"analysis": analysis
}
save_to_db(query, country, save_data)
# 3) DB์—์„œ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
loaded_data, timestamp = load_from_db(query, country)
if not loaded_data:
return "DB์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค."
# 4) ์ตœ์ข… ์ถœ๋ ฅ ์ค€๋น„
out = f"## [์‚ฌ์šฉ์ž ์ •์˜ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ]\n\n"
out += f"**๊ฒ€์ƒ‰์–ด**: {query}\n\n"
out += f"**๊ตญ๊ฐ€**: {country}\n\n"
out += f"**ํƒ€์ž„์Šคํƒฌํ”„**: {timestamp}\n\n"
arts = loaded_data.get("articles", [])
analy = loaded_data.get("analysis", "")
out += display_results(arts)
out += f"### ๋‰ด์Šค ๊ฐ์„ฑ ๋ถ„์„\n{analy}\n"
return out
########################################
# API ์ธ์ฆ ์„ค์ •
########################################
ACCESS_TOKEN = os.getenv("HF_TOKEN")
if not ACCESS_TOKEN:
raise ValueError("HF_TOKEN ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")
client = OpenAI(
base_url="https://api-inference.huggingface.co/v1/",
api_key=ACCESS_TOKEN,
)
API_KEY = os.getenv("SERPHOUSE_API_KEY")
########################################
# ๊ตญ๊ฐ€๋ณ„ ์„ค์ •
########################################
COUNTRY_LANGUAGES = {
"United States": "en",
"KOREA": "ko",
"United Kingdom": "en",
"Taiwan": "zh-TW",
"Canada": "en",
"Australia": "en",
"Germany": "de",
"France": "fr",
"Japan": "ja",
"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": "Croatia",
"Slovakia": "Slovakia",
"Bulgaria": "bg",
"Serbia": "sr",
"Estonia": "et",
"Latvia": "lv",
"Lithuania": "lt",
"Slovenia": "sl",
"Luxembourg": "Luxembourg",
"Malta": "Malta",
"Cyprus": "Cyprus",
"Iceland": "Iceland"
}
COUNTRY_LOCATIONS = {
"United States": "United States",
"KOREA": "kr",
"United Kingdom": "United Kingdom",
"Taiwan": "Taiwan",
"Canada": "Canada",
"Australia": "Australia",
"Germany": "Germany",
"France": "France",
"Japan": "Japan",
"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": "et",
"Latvia": "lv",
"Lithuania": "lt",
"Slovenia": "sl",
"Luxembourg": "Luxembourg",
"Malta": "Malta",
"Cyprus": "Cyprus",
"Iceland": "Iceland"
}
@lru_cache(maxsize=100)
def translate_query(query, country):
"""
๋น„๊ณต์‹ Google Translation API๋ฅผ ์‚ฌ์šฉํ•ด ๋Œ€์ƒ ๊ตญ๊ฐ€์˜ ์–ธ์–ด๋กœ ๊ฒ€์ƒ‰์–ด ๋ฒˆ์—ญ.
๋ฒˆ์—ญ์— ์‹คํŒจํ•˜๊ฑฐ๋‚˜ ์˜์–ด์ผ ๊ฒฝ์šฐ ์›๋ณธ ๊ฒ€์ƒ‰์–ด ๋ฐ˜ํ™˜.
"""
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
def is_english(text):
"""
๋ฌธ์ž์—ด์ด ์ฃผ๋กœ ์˜์–ด์ธ์ง€ ํ™•์ธ (๋ฌธ์ž ์ฝ”๋“œ ๋ฒ”์œ„ ํ™•์ธ).
"""
return all(ord(char) < 128 for char in text.replace(' ', '').replace('-', '').replace('_', ''))
def search_serphouse(query, country, page=1, num_result=10):
"""
SerpHouse API์— ์‹ค์‹œ๊ฐ„ ๊ฒ€์ƒ‰ ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉฐ,
'news' ํƒญ(๋‚ ์งœ์ˆœ ์ •๋ ฌ)๋กœ ๊ฒ€์ƒ‰.
๋ฐ˜ํ™˜๊ฐ’์€ 'results' ๋˜๋Š” 'error'๊ฐ€ ํฌํ•จ๋œ dict.
"""
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": str(page),
"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=5,
backoff_factor=1,
status_forcelist=[500, 502, 503, 504, 429],
allowed_methods=["POST"]
)
adapter = HTTPAdapter(max_retries=retries)
session.mount('http://', adapter)
session.mount('https://', adapter)
response = session.post(
url,
json=payload,
headers=headers,
timeout=(30, 30)
)
response.raise_for_status()
return {"results": response.json(), "translated_query": translated_query}
except requests.exceptions.Timeout:
return {
"error": "๊ฒ€์ƒ‰ ์‹œ๊ฐ„์ด ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.",
"translated_query": query
}
except requests.exceptions.RequestException as e:
return {
"error": f"๊ฒ€์ƒ‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}",
"translated_query": query
}
except Exception as e:
return {
"error": f"์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}",
"translated_query": query
}
def format_results_from_raw(response_data):
"""
SerpHouse API์˜ ์‘๋‹ต ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜์—ฌ (์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€, ๊ธฐ์‚ฌ ๋ชฉ๋ก)์„ ๋ฐ˜ํ™˜.
"""
if "error" in response_data:
return "์˜ค๋ฅ˜: " + 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 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)
# Refined, modern, and sleek custom CSS
css = """
body {
background: linear-gradient(to bottom right, #f9fafb, #ffffff);
font-family: 'Arial', sans-serif;
}
/* Hide default Gradio footer */
footer {
visibility: hidden;
}
/* Header/Status area */
#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 */
#results_area {
padding: 10px;
margin-top: 10px;
}
/* Tabs style */
.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;
background-color: #f0f0f0 !important;
border: 1px solid #ccc !important;
border-radius: 5px !important;
margin-right: 5px !important;
}
.tab-nav button.selected {
border-bottom: 2px solid #1f77b4 !important;
background-color: #e6f2fa !important;
color: #1f77b4 !important;
}
/* Status message styling */
#status_area .markdown-text {
font-size: 1.1em;
color: #2c3e50;
padding: 10px 0;
}
/* Main container grouping */
.group {
border: 1px solid #eee;
padding: 15px;
margin-bottom: 15px;
border-radius: 5px;
background: white;
transition: all 0.3s ease;
opacity: 0;
transform: translateY(20px);
}
.group.visible {
opacity: 1;
transform: translateY(0);
}
/* Buttons */
.primary-btn {
background: #1f77b4 !important;
border: none !important;
color: #fff !important;
border-radius: 5px !important;
padding: 10px 20px !important;
cursor: pointer !important;
}
.primary-btn:hover {
background: #155a8c !important;
}
.secondary-btn {
background: #f0f0f0 !important;
border: 1px solid #ccc !important;
color: #333 !important;
border-radius: 5px !important;
padding: 10px 20px !important;
cursor: pointer !important;
}
.secondary-btn:hover {
background: #e0e0e0 !important;
}
/* Input fields */
.textbox {
border: 1px solid #ddd !important;
border-radius: 4px !important;
}
/* Progress bar container */
.progress-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 6px;
background: #e0e0e0;
z-index: 1000;
}
/* Progress bar */
.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 */
.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);
}
/* Progress bar animation */
@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);
}
}
/* Loading state */
.loading {
opacity: 0.7;
pointer-events: none;
transition: opacity 0.3s ease;
}
/* Responsive design for smaller screens */
@media (max-width: 768px) {
.group {
padding: 10px;
margin-bottom: 15px;
}
.progress-text {
font-size: 12px;
padding: 3px 10px;
}
}
/* Example section styling */
.examples-table {
margin-top: 10px !important;
margin-bottom: 20px !important;
}
.examples-table button {
background-color: #f0f0f0 !important;
border: 1px solid #ddd !important;
border-radius: 4px !important;
padding: 5px 10px !important;
margin: 2px !important;
transition: all 0.3s ease !important;
}
.examples-table button:hover {
background-color: #e0e0e0 !important;
transform: translateY(-1px) !important;
box-shadow: 0 2px 5px rgba(0,0,0,0.1) !important;
}
.examples-table .label {
font-weight: bold !important;
color: #444 !important;
margin-bottom: 5px !important;
}
"""
# --- Gradio ์ธํ„ฐํŽ˜์ด์Šค (UI ๋ถ€๋ถ„) ---
with gr.Blocks(css=css, title="NewsAI ์„œ๋น„์Šค") as iface:
# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™” (init_db() ํ˜ธ์ถœ)
init_db()
gr.HTML("""<a href="https://visitorbadge.io/status?path=https%3A%2F%2Fopenfree-MoneyRadar.hf.space">
<img src="https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Fopenfree-MoneyRadar.hf.space&countColor=%23263759" />
</a>""")
with gr.Tabs():
with gr.Tab("MoneyRadar"):
# ์‚ฌ์šฉ ๋ฐฉ๋ฒ• ๋ฐ ๊ธฐ๋Šฅ ์„ค๋ช… (ํ•œ๊ธ€)
gr.Markdown(
"""
## MoneyRadar
์ตœ์‹  24์‹œ๊ฐ„ ๋‚ด ์ƒ์œ„ 100๊ฐœ์˜ ์šฐ์„ ์ˆœ์œ„ ๋‰ด์Šค๋ฅผ ์ž๋™์œผ๋กœ ์ถ”์ถœํ•˜์—ฌ
์ˆ˜์ต ๊ธฐํšŒ๋ฅผ ํฌ์ฐฉํ•ฉ๋‹ˆ๋‹ค.
**์„œ๋น„์Šค ์‚ฌ์šฉ ๋ฐฉ๋ฒ•**:
1. **์‚ฌ์šฉ์ž ์ •์˜ ๊ฒ€์ƒ‰**: ํ‚ค์›Œ๋“œ๋ฅผ ์ž…๋ ฅํ•˜๊ณ  ๋Œ€์ƒ ๊ตญ๊ฐ€๋ฅผ ์„ ํƒํ•˜์—ฌ ์ตœ์‹  ๋‰ด์Šค๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
์‹œ์Šคํ…œ์ด ์ž๋™์œผ๋กœ ๊ฐ์„ฑ ๋ถ„์„์„ ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
2. **์ „์ฒด ๋ถ„์„ ์ข…ํ•ฉ ๋ณด๊ณ ์„œ ์ƒ์„ฑ**: ์•„๋ž˜ ์ž‘์—…์„ ์ž๋™์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
- ์‚ฌ์ „ ์ •์˜๋œ ๋ชจ๋“  ํšŒ์‚ฌ๋ฅผ ๋ณ‘๋ ฌ๋กœ ๊ฒ€์ƒ‰
- ๊ธฐ์‚ฌ์™€ ๊ฐ์„ฑ ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ
- ์ „์ฒด ๊ฒฐ๊ณผ๋ฅผ ์ข…ํ•ฉ ๋ณด๊ณ ์„œ๋กœ ํ‘œ์‹œ
3. **๊ฐœ๋ณ„ ํšŒ์‚ฌ ๊ฒ€์ƒ‰/๋ถˆ๋Ÿฌ์˜ค๊ธฐ**:
- **๊ฒ€์ƒ‰**: ์„ ํƒํ•œ ํšŒ์‚ฌ์— ๋Œ€ํ•ด Google์—์„œ ์ตœ์‹  ๋‰ด์Šค๋ฅผ ๊ฒ€์ƒ‰ ๋ฐ ๋ถ„์„
- **DB ๋ถˆ๋Ÿฌ์˜ค๊ธฐ**: ๋กœ์ปฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์ €์žฅ๋œ ์ตœ์‹  ๋‰ด์Šค์™€ ๊ฐ์„ฑ ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ๋ถˆ๋Ÿฌ์˜ต๋‹ˆ๋‹ค.
**์ฃผ์š” ๊ธฐ๋Šฅ**:
- **์‹ค์‹œ๊ฐ„ ๋‰ด์Šค ์Šคํฌ๋ž˜ํ•‘**: ์—ฌ๋Ÿฌ ์ง€์—ญ์—์„œ ์ตœ์‹  ๊ธฐ์‚ฌ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
- **๊ณ ๊ธ‰ ๊ฐ์„ฑ ๋ถ„์„**: ์ตœ์‹  NLP ๋ชจ๋ธ์„ ํ™œ์šฉํ•œ ๊ฐ์„ฑ ๋ถ„์„์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
- **๋ฐ์ดํ„ฐ ์˜์†์„ฑ**: ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ SQLite ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ž๋™ ์ €์žฅ ๋ฐ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ.
- **์œ ์—ฐ์„ฑ**: ์‚ฌ์ „ ์ •์˜๋œ ํ‚ค์›Œ๋“œ๋ฟ ์•„๋‹ˆ๋ผ ์›ํ•˜๋Š” ํ‚ค์›Œ๋“œ/๊ตญ๊ฐ€ ๊ฒ€์ƒ‰์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
**์ปค๋ฎค๋‹ˆํ‹ฐ**: https://discord.gg/openfreeai
---
"""
)
# ์‚ฌ์šฉ์ž ์ •์˜ ๊ฒ€์ƒ‰ ์„น์…˜
with gr.Group():
gr.Markdown("### ์‚ฌ์šฉ์ž ์ •์˜ ๊ฒ€์ƒ‰")
with gr.Row():
with gr.Column():
user_input = gr.Textbox(
label="ํ‚ค์›Œ๋“œ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”",
placeholder="์˜ˆ: Apple, Samsung ๋“ฑ",
elem_classes="textbox"
)
with gr.Column():
country_selection = gr.Dropdown(
choices=list(COUNTRY_LOCATIONS.keys()),
value="United States",
label="๊ตญ๊ฐ€ ์„ ํƒ"
)
with gr.Column():
custom_search_btn = gr.Button(
"๊ฒ€์ƒ‰",
variant="primary",
elem_classes="primary-btn"
)
custom_search_output = gr.Markdown()
custom_search_btn.click(
fn=search_custom,
inputs=[user_input, country_selection],
outputs=custom_search_output
)
# ์ „์ฒด ๋ณด๊ณ ์„œ ์ƒ์„ฑ์„ ์œ„ํ•œ ๋ฒ„ํŠผ
with gr.Row():
full_report_btn = gr.Button(
"์ „์ฒด ๋ถ„์„ ์ข…ํ•ฉ ๋ณด๊ณ ์„œ ์ƒ์„ฑ",
variant="primary",
elem_classes="primary-btn"
)
full_report_display = gr.Markdown()
full_report_btn.click(
fn=full_summary_report,
outputs=full_report_display
)
# ๊ฐœ๋ณ„ ํšŒ์‚ฌ์— ๋Œ€ํ•œ ๊ฒ€์ƒ‰/๋ถˆ๋Ÿฌ์˜ค๊ธฐ
with gr.Column():
for i in range(0, len(KOREAN_COMPANIES), 2):
with gr.Row():
# ์™ผ์ชฝ ์ปฌ๋Ÿผ
with gr.Column():
company = KOREAN_COMPANIES[i]
with gr.Group():
gr.Markdown(f"### {company}")
with gr.Row():
search_btn = gr.Button(
"๊ฒ€์ƒ‰",
variant="primary",
elem_classes="primary-btn"
)
load_btn = gr.Button(
"DB ๋ถˆ๋Ÿฌ์˜ค๊ธฐ",
variant="secondary",
elem_classes="secondary-btn"
)
result_display = gr.Markdown()
search_btn.click(
fn=lambda c=company: search_company(c),
outputs=result_display
)
load_btn.click(
fn=lambda c=company: load_company(c),
outputs=result_display
)
# ์˜ค๋ฅธ์ชฝ ์ปฌ๋Ÿผ (์กด์žฌํ•  ๊ฒฝ์šฐ)
if i + 1 < len(KOREAN_COMPANIES):
with gr.Column():
company = KOREAN_COMPANIES[i + 1]
with gr.Group():
gr.Markdown(f"### {company}")
with gr.Row():
search_btn = gr.Button(
"๊ฒ€์ƒ‰",
variant="primary",
elem_classes="primary-btn"
)
load_btn = gr.Button(
"DB ๋ถˆ๋Ÿฌ์˜ค๊ธฐ",
variant="secondary",
elem_classes="secondary-btn"
)
result_display = gr.Markdown()
search_btn.click(
fn=lambda c=company: search_company(c),
outputs=result_display
)
load_btn.click(
fn=lambda c=company: load_company(c),
outputs=result_display
)
# Gradio ์ธํ„ฐํŽ˜์ด์Šค ์‹คํ–‰
iface.launch(
server_name="0.0.0.0",
server_port=7860,
share=True,
ssl_verify=False,
show_error=True
)