hanjunjung commited on
Commit
b44da90
ยท
1 Parent(s): a4bb82f
Files changed (3) hide show
  1. app.py +162 -312
  2. packages.txt +0 -5
  3. requirements.txt +1 -2
app.py CHANGED
@@ -3,17 +3,11 @@ import time
3
  import base64
4
  import json
5
  import traceback
6
- import subprocess
7
- import sys
8
  from typing import Tuple, Optional
9
  import gradio as gr
10
  import anthropic
11
- from selenium import webdriver
12
- from selenium.webdriver.chrome.options import Options
13
- from selenium.webdriver.common.by import By
14
- from selenium.webdriver.support.ui import WebDriverWait
15
- from selenium.webdriver.support import expected_conditions as EC
16
- from selenium.common.exceptions import TimeoutException, NoSuchElementException
17
  from concurrent.futures import ThreadPoolExecutor
18
  import threading
19
 
@@ -21,50 +15,6 @@ import threading
21
  IS_HUGGINGFACE = os.getenv("SPACE_ID") is not None
22
  print(f'IS_HUGGINGFACE = {IS_HUGGINGFACE}')
23
 
24
- # Hugging Face ํ™˜๊ฒฝ์—์„œ Chrome ์„ค์น˜
25
- def install_chrome():
26
- if not IS_HUGGINGFACE:
27
- return True
28
-
29
- try:
30
- print("Chrome ์„ค์น˜ ์‹œ์ž‘...")
31
-
32
- # Google Chrome ๋‹ค์šด๋กœ๋“œ ๋ฐ ์„ค์น˜
33
- commands = [
34
- "wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add -",
35
- "echo 'deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main' | tee /etc/apt/sources.list.d/google-chrome.list",
36
- "apt-get update",
37
- "apt-get install -y google-chrome-stable"
38
- ]
39
-
40
- for cmd in commands:
41
- print(f"์‹คํ–‰ ์ค‘: {cmd}")
42
- result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
43
- if result.returncode != 0:
44
- print(f"๋ช…๋ น์–ด ์‹คํŒจ: {cmd}")
45
- print(f"์˜ค๋ฅ˜: {result.stderr}")
46
- else:
47
- print(f"์„ฑ๊ณต: {cmd}")
48
-
49
- # Chrome ๋ฒ„์ „ ํ™•์ธ
50
- result = subprocess.run("google-chrome --version", shell=True, capture_output=True, text=True)
51
- if result.returncode == 0:
52
- print(f"Chrome ์„ค์น˜ ์™„๋ฃŒ: {result.stdout.strip()}")
53
- return True
54
- else:
55
- print("Chrome ์„ค์น˜ ์‹คํŒจ")
56
- return False
57
-
58
- except Exception as e:
59
- print(f"Chrome ์„ค์น˜ ์ค‘ ์˜ค๋ฅ˜: {e}")
60
- return False
61
-
62
- # ์‹œ์ž‘ ์‹œ Chrome ์„ค์น˜
63
- if IS_HUGGINGFACE:
64
- chrome_installed = install_chrome()
65
- if not chrome_installed:
66
- print("Chrome ์„ค์น˜์— ์‹คํŒจํ–ˆ์ง€๋งŒ ๊ณ„์† ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค...")
67
-
68
  # Claude API ์„ค์ • (Hugging Face Secrets์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ)
69
  ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
70
 
@@ -73,6 +23,33 @@ if not ANTHROPIC_API_KEY:
73
  else:
74
  print("Claude API ํ‚ค ํ™•์ธ ์™„๋ฃŒ")
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  # Claude ํด๋ผ์ด์–ธํŠธ๋ฅผ ์ƒ์„ฑ
77
  def create_claude_client():
78
  try:
@@ -83,229 +60,81 @@ def create_claude_client():
83
  print(f"Claude API ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ ์‹คํŒจ: {e}")
84
  return None
85
 
86
- # Google Trends ์ž๋™ํ™” ํด๋ž˜์Šค
87
  class GoogleTrendsAutomator:
88
  def __init__(self):
89
- self.driver = None
 
90
 
91
- # Chrome ๋“œ๋ผ์ด๋ฒ„ ์„ค์ •
92
- def setup_driver(self):
93
- options = Options()
94
-
95
- # Hugging Face Space ํ™˜๊ฒฝ ์„ค์ •
96
- if IS_HUGGINGFACE:
97
- print("Hugging Face Space ํ™˜๊ฒฝ, Chrome ๋ธŒ๋ผ์šฐ์ €๋กœ ์‹คํ–‰")
98
- # Chrome ๋ฐ”์ด๋„ˆ๋ฆฌ ๊ฒฝ๋กœ ์„ค์ •
99
- chrome_paths = [
100
- "/usr/bin/google-chrome",
101
- "/usr/bin/google-chrome-stable",
102
- "/usr/bin/chromium",
103
- "/usr/bin/chromium-browser"
104
- ]
105
-
106
- chrome_binary = None
107
- for path in chrome_paths:
108
- if os.path.exists(path):
109
- chrome_binary = path
110
- print(f"Chrome ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐœ๊ฒฌ: {path}")
111
- break
112
-
113
- if chrome_binary:
114
- options.binary_location = chrome_binary
115
-
116
- options.add_argument("--headless=new")
117
- options.add_argument("--no-sandbox")
118
- options.add_argument("--disable-dev-shm-usage")
119
- options.add_argument("--disable-gpu")
120
- options.add_argument("--disable-software-rasterizer")
121
- options.add_argument("--remote-debugging-port=9222")
122
- options.add_argument("--single-process")
123
- options.add_argument("--disable-background-timer-throttling")
124
- options.add_argument("--disable-backgrounding-occluded-windows")
125
- options.add_argument("--disable-renderer-backgrounding")
126
- else:
127
- options.add_argument("--headless")
128
-
129
- # ์†๋„ ์ตœ์ ํ™” ์„ค์ •
130
- options.add_argument("--window-size=1280,720")
131
- options.add_argument("--disable-blink-features=AutomationControlled")
132
- options.add_argument("--disable-extensions")
133
- options.add_argument("--disable-web-security")
134
- options.add_argument("--allow-running-insecure-content")
135
- options.add_argument("--disable-features=TranslateUI")
136
- options.add_argument("--disable-ipc-flooding-protection")
137
-
138
- # ๋„คํŠธ์›Œํฌ ์ตœ์ ํ™”
139
- options.add_argument("--aggressive-cache-discard")
140
- options.add_argument("--disable-background-networking")
141
-
142
- # ๋ฉ”๋ชจ๋ฆฌ ์ตœ์ ํ™”
143
- options.add_argument("--memory-pressure-off")
144
- options.add_argument("--max_old_space_size=2048")
145
-
146
- # ์ด๋ฏธ์ง€/CSS ๋น„ํ™œ์„ฑํ™”๋กœ ๋กœ๋”ฉ ์†๋„ ํ–ฅ์ƒ
147
- prefs = {
148
- "profile.managed_default_content_settings.images": 2,
149
- "profile.default_content_setting_values.notifications": 2,
150
- }
151
- options.add_experimental_option("prefs", prefs)
152
-
153
- # ๋ด‡ ๊ฐ์ง€ ํšŒํ”ผ
154
- options.add_argument("--user-agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
155
- options.add_experimental_option("excludeSwitches", ["enable-automation"])
156
- options.add_experimental_option('useAutomationExtension', False)
157
-
158
- # ์–ธ์–ด ์„ค์ •
159
- options.add_argument("--lang=ko")
160
- options.add_argument("--accept-lang=ko-KR,ko;q=0.9,en;q=0.8")
161
-
162
  try:
163
- # Hugging Face ํ™˜๊ฒฝ์—์„œ ChromeDriver ์„ค์ •
164
- if IS_HUGGINGFACE:
165
- try:
166
- # webdriver-manager ์‚ฌ์šฉ
167
- from webdriver_manager.chrome import ChromeDriverManager
168
- from selenium.webdriver.chrome.service import Service
169
-
170
- print("ChromeDriver ์ž๋™ ์„ค์น˜ ์ค‘...")
171
- service = Service(ChromeDriverManager().install())
172
- self.driver = webdriver.Chrome(service=service, options=options)
173
- print("ChromeDriver ์„ค์น˜ ๋ฐ ์ดˆ๊ธฐํ™” ์„ฑ๊ณต")
174
-
175
- except Exception as e:
176
- print(f"webdriver-manager ์‹คํŒจ: {e}")
177
- # ์‹œ์Šคํ…œ ChromeDriver ์‹œ๋„
178
- try:
179
- self.driver = webdriver.Chrome(options=options)
180
- print("์‹œ์Šคํ…œ ChromeDriver ์‚ฌ์šฉ ์„ฑ๊ณต")
181
- except Exception as sys_e:
182
- print(f"์‹œ์Šคํ…œ ChromeDriver๋„ ์‹คํŒจ: {sys_e}")
183
- return False
184
- else:
185
- # ๋กœ์ปฌ ํ™˜๊ฒฝ
186
- try:
187
- from webdriver_manager.chrome import ChromeDriverManager
188
- from selenium.webdriver.chrome.service import Service
189
-
190
- service = Service(ChromeDriverManager().install())
191
- self.driver = webdriver.Chrome(service=service, options=options)
192
- print("๋กœ์ปฌ ChromeDriver ์„ค์น˜ ์„ฑ๊ณต")
193
- except:
194
- self.driver = webdriver.Chrome(options=options)
195
- print("๋กœ์ปฌ ์‹œ์Šคํ…œ ChromeDriver ์‚ฌ์šฉ")
196
 
197
- # ํƒ€์ž„์•„์›ƒ ์„ค์ •
198
- self.driver.set_page_load_timeout(20 if IS_HUGGINGFACE else 15)
199
- self.driver.implicitly_wait(3)
200
 
201
- # ๋ด‡ ๊ฐ์ง€ ๋ฐฉ์ง€ ์Šคํฌ๋ฆฝํŠธ
202
- try:
203
- self.driver.execute_script("""
204
- Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
205
- Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3, 4, 5]});
206
- Object.defineProperty(navigator, 'languages', {get: () => ['ko-KR', 'ko', 'en']});
207
- """)
208
- except Exception as script_error:
209
- print(f"๋ด‡ ๊ฐ์ง€ ๋ฐฉ์ง€ ์Šคํฌ๋ฆฝํŠธ ์‹คํŒจ (๋ฌด์‹œ): {script_error}")
210
 
 
211
  return True
212
 
213
  except Exception as e:
214
- print(f"๋“œ๋ผ์ด๋ฒ„ ์„ค์ • ์ตœ์ข… ์‹คํŒจ: {e}")
215
  return False
216
 
217
- # ๋น ๋ฅด๊ฒŒ Element ์ฐพ๊ธฐ, Timeout ๋‹จ์ถ• ๊ณ ๋ ค
218
- def safe_find_element(self, selectors: list, timeout: int = 3):
219
- for selector_type, selector in selectors:
220
  try:
221
- if selector_type == "xpath":
222
- element = WebDriverWait(self.driver, timeout).until(
223
- EC.presence_of_element_located((By.XPATH, selector))
224
- )
225
- elif selector_type == "css":
226
- element = WebDriverWait(self.driver, timeout).until(
227
- EC.presence_of_element_located((By.CSS_SELECTOR, selector))
228
- )
229
- elif selector_type == "text":
230
- element = WebDriverWait(self.driver, timeout).until(
231
- EC.presence_of_element_located((By.XPATH, f"//*[contains(text(), '{selector}')]"))
232
- )
233
-
234
- if element:
235
- return element
236
- except:
237
- continue
238
- return None
239
-
240
- # ์ฆ‰์‹œ ํด๋ฆญ
241
- def safe_click(self, selectors: list, timeout: int = 3) -> bool:
242
- element = self.safe_find_element(selectors, timeout)
243
- if element:
244
- try:
245
- # JavaScript ํด๋ฆญ ์šฐ์„  ์‹œ๋„ (๋” ๋น ๋ฆ„)
246
- self.driver.execute_script("arguments[0].click();", element)
247
  return True
248
  except:
249
- try:
250
- element.click()
251
- return True
252
- except:
253
- return False
254
  return False
255
 
256
- # ๋ณ‘๋ ฌ๋กœ ์„ค์ • ๋ณ€๊ฒฝ ์‹œ๋„
257
- def parallel_change_settings(self, region: str, period: str, category: str, progress_callback=None) -> dict:
258
- results = {
259
- 'region': False,
260
- 'period': False,
261
- 'category': False,
262
- 'errors': []
263
- }
264
-
265
- # ์ˆœ์ฐจ์ ์œผ๋กœ ๋ณ€๊ฒฝ (๋ณ‘๋ ฌ์€ selenium์—์„œ ์•ˆ์ „ํ•˜์ง€ ์•Š์Œ)
266
- try:
267
- if region != "๋Œ€ํ•œ๋ฏผ๊ตญ":
268
- if progress_callback:
269
- progress_callback("์ง€์—ญ ์„ค์ • ๋ณ€๊ฒฝ ์ค‘...")
270
- results['region'] = self.change_region(region)
271
- time.sleep(0.5)
272
- else:
273
- results['region'] = True
274
-
275
- if period != "์ง€๋‚œ 24์‹œ๊ฐ„":
276
- if progress_callback:
277
- progress_callback("๊ธฐ๊ฐ„ ์„ค์ • ๋ณ€๊ฒฝ ์ค‘...")
278
- results['period'] = self.change_time_period(period)
279
- time.sleep(0.5)
280
- else:
281
- results['period'] = True
282
-
283
- if category != "๋ชจ๋“  ์นดํ…Œ๊ณ ๋ฆฌ":
284
- if progress_callback:
285
- progress_callback("์นดํ…Œ๊ณ ๋ฆฌ ์„ค์ • ๋ณ€๊ฒฝ ์ค‘...")
286
- results['category'] = self.change_category(category)
287
- time.sleep(0.5)
288
- else:
289
- results['category'] = True
290
-
291
- except Exception as e:
292
- results['errors'].append(str(e))
293
-
294
- return results
295
-
296
  # ์ง€์—ญ ๋ณ€๊ฒฝ
297
- def change_region(self, target_region: str) -> bool:
298
  try:
299
  region_selectors = [
300
- ("xpath", "//button[@aria-label='๋Œ€ํ•œ๋ฏผ๊ตญ, ์œ„์น˜ ์„ ํƒ']"),
301
- ("xpath", "//button[contains(@aria-label, '์œ„์น˜ ์„ ํƒ')]"),
302
- ("xpath", "//span[contains(text(), '๋Œ€ํ•œ๋ฏผ๊ตญ')]//parent::button"),
303
  ]
304
 
305
- if not self.safe_click(region_selectors):
306
  return False
307
 
308
- time.sleep(1)
309
 
310
  region_mapping = {
311
  "์ „์„ธ๊ณ„": ["์ „ ์„ธ๊ณ„", "Worldwide"],
@@ -317,11 +146,12 @@ class GoogleTrendsAutomator:
317
  if target_region in region_mapping:
318
  for region_text in region_mapping[target_region]:
319
  region_option_selectors = [
320
- ("xpath", f"//span[contains(text(), '{region_text}')]"),
321
- ("xpath", f"//*[contains(text(), '{region_text}')]"),
 
322
  ]
323
 
324
- if self.safe_click(region_option_selectors, timeout=2):
325
  return True
326
 
327
  return False
@@ -331,17 +161,17 @@ class GoogleTrendsAutomator:
331
  return False
332
 
333
  # ๊ธฐ๊ฐ„ ๋ณ€๊ฒฝ
334
- def change_time_period(self, target_period: str) -> bool:
335
  try:
336
  period_selectors = [
337
- ("xpath", "//button[contains(@aria-label, '๊ธฐ๊ฐ„ ์„ ํƒ')]"),
338
- ("xpath", "//span[contains(text(), '์ง€๋‚œ 24์‹œ๊ฐ„')]//parent::button"),
339
  ]
340
 
341
- if not self.safe_click(period_selectors):
342
  return False
343
 
344
- time.sleep(1)
345
 
346
  period_mapping = {
347
  "์ง€๋‚œ 1์‹œ๊ฐ„": ["์ง€๋‚œ 1์‹œ๊ฐ„"],
@@ -353,11 +183,12 @@ class GoogleTrendsAutomator:
353
  if target_period in period_mapping:
354
  for period_text in period_mapping[target_period]:
355
  period_option_selectors = [
356
- ("xpath", f"//span[contains(text(), '{period_text}')]"),
357
- ("xpath", f"//*[contains(text(), '{period_text}')]"),
 
358
  ]
359
 
360
- if self.safe_click(period_option_selectors, timeout=2):
361
  return True
362
 
363
  return False
@@ -367,17 +198,17 @@ class GoogleTrendsAutomator:
367
  return False
368
 
369
  # ์นดํ…Œ๊ณ ๋ฆฌ ๋ณ€๊ฒฝ
370
- def change_category(self, target_category: str) -> bool:
371
  try:
372
  category_selectors = [
373
- ("xpath", "//button[contains(@aria-label, '์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ')]"),
374
- ("xpath", "//span[contains(text(), '๋ชจ๋“  ์นดํ…Œ๊ณ ๋ฆฌ')]//parent::button"),
375
  ]
376
 
377
- if not self.safe_click(category_selectors):
378
  return False
379
 
380
- time.sleep(1)
381
 
382
  category_mapping = {
383
  "๊ฒŒ์ž„": ["๊ฒŒ์ž„"],
@@ -392,11 +223,12 @@ class GoogleTrendsAutomator:
392
  if target_category in category_mapping:
393
  for category_text in category_mapping[target_category]:
394
  category_option_selectors = [
395
- ("xpath", f"//span[contains(text(), '{category_text}')]"),
396
- ("xpath", f"//*[contains(text(), '{category_text}')]"),
 
397
  ]
398
 
399
- if self.safe_click(category_option_selectors, timeout=2):
400
  return True
401
 
402
  return False
@@ -406,61 +238,74 @@ class GoogleTrendsAutomator:
406
  return False
407
 
408
  # Google Trends ์บก์ฒ˜
409
- def capture_trends(self, region: str, period: str, category: str, progress_callback=None) -> Tuple[Optional[str], bool, str]:
410
  error_msg = ""
411
 
412
  try:
413
  if progress_callback:
414
- progress_callback("๋ธŒ๋ผ์šฐ์ € ๋“œ๋ผ์ด๋ฒ„ ์ดˆ๊ธฐํ™” ์ค‘...")
415
 
416
- # ๋“œ๋ผ์ด๋ฒ„ ์„ค์ •
417
- if not self.setup_driver():
418
- return None, False, "๋ธŒ๋ผ์šฐ์ € ๋“œ๋ผ์ด๋ฒ„ ์„ค์ • ์‹คํŒจ"
419
 
420
  if progress_callback:
421
  progress_callback("Google Trends ํŽ˜์ด์ง€ ์ ‘์† ์ค‘...")
422
 
423
- # Google Trends ์ ‘์† (ํ•œ๊ตญ์–ด ์ง์ ‘ URL)
424
- self.driver.get("https://trends.google.com/trending?geo=KR&hl=ko")
425
 
426
- # ๋กœ๋”ฉ ๋Œ€๊ธฐ (Hugging Face์—์„œ๋Š” ์กฐ๊ธˆ ๋” ๊ธฐ๋‹ค๋ฆผ)
427
- wait_time = 3 if IS_HUGGINGFACE else 1
428
- time.sleep(wait_time)
429
 
430
  if progress_callback:
431
  progress_callback("ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์™„๋ฃŒ, ์„ค์ • ๋ณ€๊ฒฝ ์‹œ์ž‘...")
432
 
433
- # ํŽ˜์ด์ง€ ์ค€๋น„ ํ™•์ธ (๋น ๋ฅธ ์ฒดํฌ)
434
  try:
435
- WebDriverWait(self.driver, 8).until(
436
- EC.presence_of_element_located((By.TAG_NAME, "button"))
437
- )
438
- except TimeoutException:
439
  pass
440
 
441
- # ์„ค์ • ๋ณ€๊ฒฝ (๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ)
442
- settings_result = self.parallel_change_settings(region, period, category, progress_callback)
443
-
444
- # ๊ฒฐ๊ณผ ํ™•์ธ
445
  changes_made = []
446
- if settings_result['region']:
447
- changes_made.append(f"์ง€์—ญ: {region}")
448
- if settings_result['period']:
449
- changes_made.append(f"๊ธฐ๊ฐ„: {period}")
450
- if settings_result['category']:
451
- changes_made.append(f"์นดํ…Œ๊ณ ๋ฆฌ: {category}")
452
 
453
- if settings_result['errors']:
454
- error_msg = "\n".join(settings_result['errors'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
455
 
456
  if progress_callback:
457
- progress_callback("์„ค์ • ๋ณ€๊ฒฝ ์™„๋ฃŒ, ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๋Š”์ค‘...")
458
 
459
- # ์ตœ์ข… ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ๋Œ€๊ธฐ
460
- time.sleep(2 if IS_HUGGINGFACE else 1)
461
 
462
  # ์Šคํฌ๋ฆฐ์ƒท ์ดฌ์˜
463
- screenshot = self.driver.get_screenshot_as_png()
464
  screenshot_b64 = base64.b64encode(screenshot).decode()
465
 
466
  success_msg = f"์„ค์ • ์™„๋ฃŒ: {', '.join(changes_made) if changes_made else '๊ธฐ๋ณธ ์„ค์ • ์‚ฌ์šฉ'}"
@@ -472,9 +317,10 @@ class GoogleTrendsAutomator:
472
  return None, False, error_msg
473
 
474
  finally:
475
- if self.driver:
476
  try:
477
- self.driver.quit()
 
478
  except:
479
  pass
480
 
@@ -551,6 +397,11 @@ HTML ํ˜•ํƒœ๋กœ ๊น”๋”ํ•˜๊ฒŒ ์ •๋ฆฌํ•˜๊ณ , ์ด๋ชจ์ง€๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์‹œ
551
  </div>
552
  """
553
 
 
 
 
 
 
554
  def main_analysis(region: str, period: str, category: str):
555
  # ์‹ค์‹œ๊ฐ„ ์ŠคํŠธ๋ฆฌ๋ฐ ๋ถ„์„ ํ•จ์ˆ˜ (Generator)
556
 
@@ -592,18 +443,17 @@ def main_analysis(region: str, period: str, category: str):
592
 
593
  <div style="padding: 15px; background-color: #f8f9fa; border-radius: 5px; margin-bottom: 15px;">
594
  <p><strong>์ง„ํ–‰ ์ค‘:</strong> ๋ธŒ๋ผ์šฐ์ € ๋“œ๋ผ์ด๋ฒ„ ์„ค์ • ๋ฐ ํŽ˜์ด์ง€ ๋กœ๋”ฉ...</p>
595
- {'<p style="font-size: 12px; color: #666;">Chrome ๋ธŒ๋ผ์šฐ์ € ์ดˆ๊ธฐํ™” ์ค‘...</p>' if IS_HUGGINGFACE else ''}
596
  </div>
597
  """
598
 
599
  # 4๋‹จ๊ณ„: Google Trends ์ž๋™ํ™” ์‹คํ–‰
600
  print(f"๋ถ„์„ ์‹œ์ž‘: {region} | {period} | {category}")
601
- automator = GoogleTrendsAutomator()
602
 
603
  # ๋ธŒ๋ผ์šฐ์ € ์ž‘์—…์„ ๋ณ„๋„ ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰
604
  with ThreadPoolExecutor(max_workers=1) as executor:
605
  future = executor.submit(
606
- automator.capture_trends,
607
  region, period, category, progress_callback
608
  )
609
 
@@ -615,7 +465,7 @@ def main_analysis(region: str, period: str, category: str):
615
  yield f"""
616
  <div style="text-align: center; padding: 15px; background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%); color: white; border-radius: 10px; margin-bottom: 15px;">
617
  <h4>์ž๋™ํ™” ์ง„ํ–‰ ์ค‘</h4>
618
- <p>Chrome ๋ธŒ๋ผ์šฐ์ €์—์„œ Google Trends ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์ค‘...</p>
619
  </div>
620
 
621
  <div style="padding: 15px; background-color: #f8f9fa; border-radius: 5px; margin-bottom: 15px;">
@@ -639,7 +489,7 @@ def main_analysis(region: str, period: str, category: str):
639
  </pre>
640
  <p><strong>ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•:</strong></p>
641
  <ul>
642
- <li>packages.txt ํŒŒ์ผ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋˜์—ˆ๋Š”์ง€ ํ™•์ธ</li>
643
  <li>Space๋ฅผ ์žฌ์‹œ์ž‘ํ•ด๋ณด์„ธ์š”</li>
644
  <li>์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด๋ณด์„ธ์š”</li>
645
  </ul>
@@ -685,7 +535,7 @@ def main_analysis(region: str, period: str, category: str):
685
  </div>
686
 
687
  <div style="margin-top: 15px; padding: 10px; background-color: #f9f9f9; border-radius: 5px; font-size: 12px; color: #666; text-align: center;">
688
- <p style="margin: 5px 0;"><strong>์‹คํ–‰ ํ™˜๊ฒฝ:</strong> {'Hugging Face Space (Chrome)' if IS_HUGGINGFACE else 'Local'}</p>
689
  <p style="margin: 5px 0;"><strong>AI ์—”์ง„:</strong> Claude 3.5 Sonnet</p>
690
  <p style="margin: 5px 0;"><strong>๋ฐ์ดํ„ฐ ์ถœ์ฒ˜:</strong> Google Trends ์‹ค์‹œ๊ฐ„ ์Šคํฌ๋ฆฐ์ƒท</p>
691
  </div>
@@ -733,7 +583,7 @@ def create_interface():
733
  <h1>Google Trends ์‹ค์‹œ๊ฐ„ ๋ถ„์„๊ธฐ</h1>
734
  <p>Google Trends์˜ ์‹ค์‹œ๊ฐ„ ์ธ๊ธฐ ๊ฒ€์ƒ‰์–ด๋ฅผ AI๊ฐ€ ์ž๋™์œผ๋กœ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค</p>
735
  <p style="font-size: 14px; opacity: 0.9;">๊ฐœ์„ ๋œ ๋ฐฉ์‹์œผ๋กœ ๋‹จ๊ณ„๋ณ„ ์ง„ํ–‰์ƒํ™ฉ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.</p>
736
- {'<p style="font-size: 12px; opacity: 0.7;">Hugging Face Space (Chrome ๋ธŒ๋ผ์šฐ์ €)</p>' if IS_HUGGINGFACE else ''}
737
  </div>
738
  """)
739
 
@@ -788,7 +638,7 @@ def create_interface():
788
  <h3>๋ถ„์„ ๋Œ€๊ธฐ ์ค‘</h3>
789
  <p>์œ„์˜ ์„ค์ •์„ ์„ ํƒํ•˜๊ณ  '์‹ค์‹œ๊ฐ„ ํŠธ๋ Œ๋“œ ๋ถ„์„ ์‹œ์ž‘' ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์„ธ์š”!</p>
790
  <p style="font-size: 14px;">๊ฐœ์„ ๋œ ๋ฐฉ์‹์œผ๋กœ <strong>15-30์ดˆ</strong>๋งŒ์— ๋ถ„์„์ด ์™„๋ฃŒ๋ฉ๋‹ˆ๋‹ค.</p>
791
- {'<p style="font-size: 12px; color: #666;">Chrome ๋ธŒ๋ผ์šฐ์ € ํ™˜๊ฒฝ</p>' if IS_HUGGINGFACE else ''}
792
  </div>
793
  """
794
  )
@@ -832,7 +682,7 @@ def create_interface():
832
  <p><strong>์‚ฌ์šฉ๋ฒ•:</strong> ์›ํ•˜๋Š” ์„ค์ •์„ ์„ ํƒํ•˜๊ณ  ๋ถ„์„ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์„ธ์š”</p>
833
  <p><strong>์†๋„:</strong> ์ ์ฐจ์ ์œผ๋กœ ๊ฐœ์„  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.</p>
834
  <p><strong>Powered by:</strong> Google Trends + Claude AI + ์‹ค์‹œ๊ฐ„ ์ŠคํŠธ๋ฆฌ๋ฐ</p>
835
- {'<p style="font-size: 12px;">Running on Hugging Face Spaces with Chrome Browser</p>' if IS_HUGGINGFACE else ''}
836
  </div>
837
  """)
838
 
 
3
  import base64
4
  import json
5
  import traceback
6
+ import asyncio
 
7
  from typing import Tuple, Optional
8
  import gradio as gr
9
  import anthropic
10
+ from playwright.async_api import async_playwright
 
 
 
 
 
11
  from concurrent.futures import ThreadPoolExecutor
12
  import threading
13
 
 
15
  IS_HUGGINGFACE = os.getenv("SPACE_ID") is not None
16
  print(f'IS_HUGGINGFACE = {IS_HUGGINGFACE}')
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  # Claude API ์„ค์ • (Hugging Face Secrets์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ)
19
  ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
20
 
 
23
  else:
24
  print("Claude API ํ‚ค ํ™•์ธ ์™„๋ฃŒ")
25
 
26
+ # Playwright ๋ธŒ๋ผ์šฐ์ € ์„ค์น˜ (์ตœ์ดˆ 1ํšŒ)
27
+ async def install_playwright_browsers():
28
+ if IS_HUGGINGFACE:
29
+ try:
30
+ print("Playwright ๋ธŒ๋ผ์šฐ์ € ์„ค์น˜ ์ค‘...")
31
+ import subprocess
32
+ result = subprocess.run(["playwright", "install", "chromium"], capture_output=True, text=True)
33
+ if result.returncode == 0:
34
+ print("Playwright Chromium ์„ค์น˜ ์™„๋ฃŒ")
35
+ return True
36
+ else:
37
+ print(f"Playwright ์„ค์น˜ ์‹คํŒจ: {result.stderr}")
38
+ return False
39
+ except Exception as e:
40
+ print(f"Playwright ์„ค์น˜ ์ค‘ ์˜ค๋ฅ˜: {e}")
41
+ return False
42
+ return True
43
+
44
+ # ์‹œ์ž‘ ์‹œ Playwright ๋ธŒ๋ผ์šฐ์ € ์„ค์น˜
45
+ if IS_HUGGINGFACE:
46
+ try:
47
+ browser_installed = asyncio.run(install_playwright_browsers())
48
+ if not browser_installed:
49
+ print("Playwright ๋ธŒ๋ผ์šฐ์ € ์„ค์น˜์— ์‹คํŒจํ–ˆ์ง€๋งŒ ๊ณ„์† ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค...")
50
+ except Exception as e:
51
+ print(f"Playwright ์ดˆ๊ธฐํ™” ์˜ค๋ฅ˜: {e}")
52
+
53
  # Claude ํด๋ผ์ด์–ธํŠธ๋ฅผ ์ƒ์„ฑ
54
  def create_claude_client():
55
  try:
 
60
  print(f"Claude API ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ ์‹คํŒจ: {e}")
61
  return None
62
 
63
+ # Google Trends ์ž๋™ํ™” ํด๋ž˜์Šค (Playwright ์‚ฌ์šฉ)
64
  class GoogleTrendsAutomator:
65
  def __init__(self):
66
+ self.browser = None
67
+ self.page = None
68
 
69
+ # Playwright ๋ธŒ๋ผ์šฐ์ € ์„ค์ •
70
+ async def setup_browser(self, progress_callback=None):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  try:
72
+ if progress_callback:
73
+ progress_callback("Playwright ๋ธŒ๋ผ์šฐ์ € ์ดˆ๊ธฐํ™” ์ค‘...")
74
+
75
+ self.playwright = await async_playwright().start()
76
+
77
+ # Chromium ๋ธŒ๋ผ์šฐ์ € ์‹คํ–‰
78
+ self.browser = await self.playwright.chromium.launch(
79
+ headless=True,
80
+ args=[
81
+ "--no-sandbox",
82
+ "--disable-dev-shm-usage",
83
+ "--disable-gpu",
84
+ "--disable-software-rasterizer",
85
+ "--memory-pressure-off",
86
+ "--disable-background-timer-throttling",
87
+ "--disable-backgrounding-occluded-windows",
88
+ "--disable-renderer-backgrounding",
89
+ "--disable-features=TranslateUI",
90
+ "--disable-ipc-flooding-protection",
91
+ "--disable-web-security",
92
+ "--lang=ko",
93
+ "--accept-lang=ko-KR,ko;q=0.9,en"
94
+ ]
95
+ )
 
 
 
 
 
 
 
 
 
96
 
97
+ # ์ƒˆ ํŽ˜์ด์ง€ ์ƒ์„ฑ
98
+ self.page = await self.browser.new_page()
 
99
 
100
+ # ์ด๋ฏธ์ง€ ์ฐจ๋‹จ์œผ๋กœ ์†๋„ ํ–ฅ์ƒ
101
+ await self.page.route("**/*.{png,jpg,jpeg,gif,webp,svg,ico}", lambda route: route.abort())
102
+
103
+ # User Agent ์„ค์ •
104
+ await self.page.set_extra_http_headers({
105
+ "Accept-Language": "ko-KR,ko;q=0.9,en;q=0.8"
106
+ })
 
 
107
 
108
+ print("Playwright ๋ธŒ๋ผ์šฐ์ € ์ดˆ๊ธฐํ™” ์„ฑ๊ณต")
109
  return True
110
 
111
  except Exception as e:
112
+ print(f"Playwright ๋ธŒ๋ผ์šฐ์ € ์„ค์ • ์‹คํŒจ: {e}")
113
  return False
114
 
115
+ # ์•ˆ์ „ํ•œ ํด๋ฆญ
116
+ async def safe_click(self, selectors: list, timeout: int = 3000) -> bool:
117
+ for selector in selectors:
118
  try:
119
+ await self.page.click(selector, timeout=timeout)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  return True
121
  except:
122
+ continue
 
 
 
 
123
  return False
124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  # ์ง€์—ญ ๋ณ€๊ฒฝ
126
+ async def change_region(self, target_region: str) -> bool:
127
  try:
128
  region_selectors = [
129
+ "button[aria-label*='์œ„์น˜ ์„ ํƒ']",
130
+ "button[aria-label*='๋Œ€ํ•œ๋ฏผ๊ตญ']",
131
+ "span:has-text('๋Œ€ํ•œ๋ฏผ๊ตญ') >> xpath=.."
132
  ]
133
 
134
+ if not await self.safe_click(region_selectors):
135
  return False
136
 
137
+ await self.page.wait_for_timeout(1000)
138
 
139
  region_mapping = {
140
  "์ „์„ธ๊ณ„": ["์ „ ์„ธ๊ณ„", "Worldwide"],
 
146
  if target_region in region_mapping:
147
  for region_text in region_mapping[target_region]:
148
  region_option_selectors = [
149
+ f"span:has-text('{region_text}')",
150
+ f"text={region_text}",
151
+ f"*:has-text('{region_text}')"
152
  ]
153
 
154
+ if await self.safe_click(region_option_selectors, timeout=2000):
155
  return True
156
 
157
  return False
 
161
  return False
162
 
163
  # ๊ธฐ๊ฐ„ ๋ณ€๊ฒฝ
164
+ async def change_time_period(self, target_period: str) -> bool:
165
  try:
166
  period_selectors = [
167
+ "button[aria-label*='๊ธฐ๊ฐ„ ์„ ํƒ']",
168
+ "span:has-text('์ง€๋‚œ 24์‹œ๊ฐ„') >> xpath=.."
169
  ]
170
 
171
+ if not await self.safe_click(period_selectors):
172
  return False
173
 
174
+ await self.page.wait_for_timeout(1000)
175
 
176
  period_mapping = {
177
  "์ง€๋‚œ 1์‹œ๊ฐ„": ["์ง€๋‚œ 1์‹œ๊ฐ„"],
 
183
  if target_period in period_mapping:
184
  for period_text in period_mapping[target_period]:
185
  period_option_selectors = [
186
+ f"span:has-text('{period_text}')",
187
+ f"text={period_text}",
188
+ f"*:has-text('{period_text}')"
189
  ]
190
 
191
+ if await self.safe_click(period_option_selectors, timeout=2000):
192
  return True
193
 
194
  return False
 
198
  return False
199
 
200
  # ์นดํ…Œ๊ณ ๋ฆฌ ๋ณ€๊ฒฝ
201
+ async def change_category(self, target_category: str) -> bool:
202
  try:
203
  category_selectors = [
204
+ "button[aria-label*='์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ']",
205
+ "span:has-text('๋ชจ๋“  ์นดํ…Œ๊ณ ๋ฆฌ') >> xpath=.."
206
  ]
207
 
208
+ if not await self.safe_click(category_selectors):
209
  return False
210
 
211
+ await self.page.wait_for_timeout(1000)
212
 
213
  category_mapping = {
214
  "๊ฒŒ์ž„": ["๊ฒŒ์ž„"],
 
223
  if target_category in category_mapping:
224
  for category_text in category_mapping[target_category]:
225
  category_option_selectors = [
226
+ f"span:has-text('{category_text}')",
227
+ f"text={category_text}",
228
+ f"*:has-text('{category_text}')"
229
  ]
230
 
231
+ if await self.safe_click(category_option_selectors, timeout=2000):
232
  return True
233
 
234
  return False
 
238
  return False
239
 
240
  # Google Trends ์บก์ฒ˜
241
+ async def capture_trends(self, region: str, period: str, category: str, progress_callback=None) -> Tuple[Optional[str], bool, str]:
242
  error_msg = ""
243
 
244
  try:
245
  if progress_callback:
246
+ progress_callback("๋ธŒ๋ผ์šฐ์ € ์ดˆ๊ธฐํ™” ์ค‘...")
247
 
248
+ # ๋ธŒ๋ผ์šฐ์ € ์„ค์ •
249
+ if not await self.setup_browser(progress_callback):
250
+ return None, False, "๋ธŒ๋ผ์šฐ์ € ์„ค์ • ์‹คํŒจ"
251
 
252
  if progress_callback:
253
  progress_callback("Google Trends ํŽ˜์ด์ง€ ์ ‘์† ์ค‘...")
254
 
255
+ # Google Trends ์ ‘์†
256
+ await self.page.goto("https://trends.google.com/trending?geo=KR&hl=ko", wait_until="domcontentloaded")
257
 
258
+ # ๋กœ๋”ฉ ๋Œ€๊ธฐ
259
+ wait_time = 4000 if IS_HUGGINGFACE else 2000
260
+ await self.page.wait_for_timeout(wait_time)
261
 
262
  if progress_callback:
263
  progress_callback("ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์™„๋ฃŒ, ์„ค์ • ๋ณ€๊ฒฝ ์‹œ์ž‘...")
264
 
265
+ # ํŽ˜์ด์ง€ ์ค€๋น„ ํ™•์ธ
266
  try:
267
+ await self.page.wait_for_selector("button", timeout=10000)
268
+ except:
 
 
269
  pass
270
 
271
+ # ์„ค์ • ๋ณ€๊ฒฝ
 
 
 
272
  changes_made = []
 
 
 
 
 
 
273
 
274
+ if region != "๋Œ€ํ•œ๋ฏผ๊ตญ":
275
+ if progress_callback:
276
+ progress_callback(f"์ง€์—ญ์„ {region}์œผ๋กœ ๋ณ€๊ฒฝ ์ค‘...")
277
+ if await self.change_region(region):
278
+ changes_made.append(f"์ง€์—ญ: {region}")
279
+ await self.page.wait_for_timeout(500)
280
+ else:
281
+ error_msg += f"์ง€์—ญ ๋ณ€๊ฒฝ ์‹คํŒจ ({region})\n"
282
+
283
+ if period != "์ง€๋‚œ 24์‹œ๊ฐ„":
284
+ if progress_callback:
285
+ progress_callback(f"๊ธฐ๊ฐ„์„ {period}์œผ๋กœ ๋ณ€๊ฒฝ ์ค‘...")
286
+ if await self.change_time_period(period):
287
+ changes_made.append(f"๊ธฐ๊ฐ„: {period}")
288
+ await self.page.wait_for_timeout(500)
289
+ else:
290
+ error_msg += f"๊ธฐ๊ฐ„ ๋ณ€๊ฒฝ ์‹คํŒจ ({period})\n"
291
+
292
+ if category != "๋ชจ๋“  ์นดํ…Œ๊ณ ๋ฆฌ":
293
+ if progress_callback:
294
+ progress_callback(f"์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ {category}๋กœ ๋ณ€๊ฒฝ ์ค‘...")
295
+ if await self.change_category(category):
296
+ changes_made.append(f"์นดํ…Œ๊ณ ๋ฆฌ: {category}")
297
+ await self.page.wait_for_timeout(500)
298
+ else:
299
+ error_msg += f"์นดํ…Œ๊ณ ๋ฆฌ ๋ณ€๊ฒฝ ์‹คํŒจ ({category})\n"
300
 
301
  if progress_callback:
302
+ progress_callback("์„ค์ • ๋ณ€๊ฒฝ ์™„๋ฃŒ, ์Šคํฌ๋ฆฐ์ƒท ์ดฌ์˜ ์ค‘...")
303
 
304
+ # ์ตœ์ข… ๋กœ๋”ฉ ๋Œ€๊ธฐ
305
+ await self.page.wait_for_timeout(2000 if IS_HUGGINGFACE else 1000)
306
 
307
  # ์Šคํฌ๋ฆฐ์ƒท ์ดฌ์˜
308
+ screenshot = await self.page.screenshot(full_page=False)
309
  screenshot_b64 = base64.b64encode(screenshot).decode()
310
 
311
  success_msg = f"์„ค์ • ์™„๋ฃŒ: {', '.join(changes_made) if changes_made else '๊ธฐ๋ณธ ์„ค์ • ์‚ฌ์šฉ'}"
 
317
  return None, False, error_msg
318
 
319
  finally:
320
+ if self.browser:
321
  try:
322
+ await self.browser.close()
323
+ await self.playwright.stop()
324
  except:
325
  pass
326
 
 
397
  </div>
398
  """
399
 
400
+ # ๋™๊ธฐ ๋ž˜ํผ ํ•จ์ˆ˜
401
+ def run_async_capture(region: str, period: str, category: str, progress_callback=None):
402
+ automator = GoogleTrendsAutomator()
403
+ return asyncio.run(automator.capture_trends(region, period, category, progress_callback))
404
+
405
  def main_analysis(region: str, period: str, category: str):
406
  # ์‹ค์‹œ๊ฐ„ ์ŠคํŠธ๋ฆฌ๋ฐ ๋ถ„์„ ํ•จ์ˆ˜ (Generator)
407
 
 
443
 
444
  <div style="padding: 15px; background-color: #f8f9fa; border-radius: 5px; margin-bottom: 15px;">
445
  <p><strong>์ง„ํ–‰ ์ค‘:</strong> ๋ธŒ๋ผ์šฐ์ € ๋“œ๋ผ์ด๋ฒ„ ์„ค์ • ๋ฐ ํŽ˜์ด์ง€ ๋กœ๋”ฉ...</p>
446
+ {'<p style="font-size: 12px; color: #666;">Playwright ๋ธŒ๋ผ์šฐ์ € ์ดˆ๊ธฐํ™” ์ค‘...</p>' if IS_HUGGINGFACE else ''}
447
  </div>
448
  """
449
 
450
  # 4๋‹จ๊ณ„: Google Trends ์ž๋™ํ™” ์‹คํ–‰
451
  print(f"๋ถ„์„ ์‹œ์ž‘: {region} | {period} | {category}")
 
452
 
453
  # ๋ธŒ๋ผ์šฐ์ € ์ž‘์—…์„ ๋ณ„๋„ ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰
454
  with ThreadPoolExecutor(max_workers=1) as executor:
455
  future = executor.submit(
456
+ run_async_capture,
457
  region, period, category, progress_callback
458
  )
459
 
 
465
  yield f"""
466
  <div style="text-align: center; padding: 15px; background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%); color: white; border-radius: 10px; margin-bottom: 15px;">
467
  <h4>์ž๋™ํ™” ์ง„ํ–‰ ์ค‘</h4>
468
+ <p>Playwright ๋ธŒ๋ผ์šฐ์ €์—์„œ Google Trends ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์ค‘...</p>
469
  </div>
470
 
471
  <div style="padding: 15px; background-color: #f8f9fa; border-radius: 5px; margin-bottom: 15px;">
 
489
  </pre>
490
  <p><strong>ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•:</strong></p>
491
  <ul>
492
+ <li>Playwright ๋ธŒ๋ผ์šฐ์ € ์„ค์น˜ ํ™•์ธ</li>
493
  <li>Space๋ฅผ ์žฌ์‹œ์ž‘ํ•ด๋ณด์„ธ์š”</li>
494
  <li>์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด๋ณด์„ธ์š”</li>
495
  </ul>
 
535
  </div>
536
 
537
  <div style="margin-top: 15px; padding: 10px; background-color: #f9f9f9; border-radius: 5px; font-size: 12px; color: #666; text-align: center;">
538
+ <p style="margin: 5px 0;"><strong>์‹คํ–‰ ํ™˜๊ฒฝ:</strong> {'Hugging Face Space (Playwright)' if IS_HUGGINGFACE else 'Local'}</p>
539
  <p style="margin: 5px 0;"><strong>AI ์—”์ง„:</strong> Claude 3.5 Sonnet</p>
540
  <p style="margin: 5px 0;"><strong>๋ฐ์ดํ„ฐ ์ถœ์ฒ˜:</strong> Google Trends ์‹ค์‹œ๊ฐ„ ์Šคํฌ๋ฆฐ์ƒท</p>
541
  </div>
 
583
  <h1>Google Trends ์‹ค์‹œ๊ฐ„ ๋ถ„์„๊ธฐ</h1>
584
  <p>Google Trends์˜ ์‹ค์‹œ๊ฐ„ ์ธ๊ธฐ ๊ฒ€์ƒ‰์–ด๋ฅผ AI๊ฐ€ ์ž๋™์œผ๋กœ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค</p>
585
  <p style="font-size: 14px; opacity: 0.9;">๊ฐœ์„ ๋œ ๋ฐฉ์‹์œผ๋กœ ๋‹จ๊ณ„๋ณ„ ์ง„ํ–‰์ƒํ™ฉ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.</p>
586
+ {'<p style="font-size: 12px; opacity: 0.7;">Hugging Face Space (Playwright ๋ธŒ๋ผ์šฐ์ €)</p>' if IS_HUGGINGFACE else ''}
587
  </div>
588
  """)
589
 
 
638
  <h3>๋ถ„์„ ๋Œ€๊ธฐ ์ค‘</h3>
639
  <p>์œ„์˜ ์„ค์ •์„ ์„ ํƒํ•˜๊ณ  '์‹ค์‹œ๊ฐ„ ํŠธ๋ Œ๋“œ ๋ถ„์„ ์‹œ์ž‘' ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์„ธ์š”!</p>
640
  <p style="font-size: 14px;">๊ฐœ์„ ๋œ ๋ฐฉ์‹์œผ๋กœ <strong>15-30์ดˆ</strong>๋งŒ์— ๋ถ„์„์ด ์™„๋ฃŒ๋ฉ๋‹ˆ๋‹ค.</p>
641
+ {'<p style="font-size: 12px; color: #666;">Playwright ๋ธŒ๋ผ์šฐ์ € ํ™˜๊ฒฝ</p>' if IS_HUGGINGFACE else ''}
642
  </div>
643
  """
644
  )
 
682
  <p><strong>์‚ฌ์šฉ๋ฒ•:</strong> ์›ํ•˜๋Š” ์„ค์ •์„ ์„ ํƒํ•˜๊ณ  ๋ถ„์„ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์„ธ์š”</p>
683
  <p><strong>์†๋„:</strong> ์ ์ฐจ์ ์œผ๋กœ ๊ฐœ์„  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.</p>
684
  <p><strong>Powered by:</strong> Google Trends + Claude AI + ์‹ค์‹œ๊ฐ„ ์ŠคํŠธ๋ฆฌ๋ฐ</p>
685
+ {'<p style="font-size: 12px;">Running on Hugging Face Spaces with Playwright Browser</p>' if IS_HUGGINGFACE else ''}
686
  </div>
687
  """)
688
 
packages.txt DELETED
@@ -1,5 +0,0 @@
1
- wget
2
- unzip
3
- curl
4
- gnupg
5
- ca-certificates
 
 
 
 
 
 
requirements.txt CHANGED
@@ -1,6 +1,5 @@
1
  gradio==4.44.0
2
  anthropic==0.34.0
3
- selenium==4.25.0
4
- webdriver-manager==4.0.2
5
  python-dotenv==1.0.0
6
  Pillow==10.4.0
 
1
  gradio==4.44.0
2
  anthropic==0.34.0
3
+ playwright==1.40.0
 
4
  python-dotenv==1.0.0
5
  Pillow==10.4.0