kfkas commited on
Commit
96c34c2
·
1 Parent(s): 2b8f7ee
Files changed (2) hide show
  1. app.py +192 -86
  2. requirements.txt +2 -0
app.py CHANGED
@@ -3,28 +3,27 @@ import shutil
3
  import cv2
4
  import base64
5
  import uuid
6
- import re
7
- import time
8
- import pandas as pd
9
  from flask import Flask
 
 
10
  import gradio as gr
11
-
12
- # Selenium 관련 임포트 (구글 리뷰 크롤링용)
13
  from selenium import webdriver
14
  from selenium.webdriver.common.by import By
15
  from selenium.webdriver.chrome.service import Service
16
  from selenium.webdriver.support.ui import WebDriverWait
17
  from selenium.webdriver.support import expected_conditions as EC
18
- from selenium.common.exceptions import NoSuchElementException, TimeoutException
19
  from webdriver_manager.chrome import ChromeDriverManager
 
 
20
 
21
- # --- GoogleReviewManager: 구글 리뷰 크롤링 및 포맷팅 ---
22
  class GoogleReviewManager:
23
  """
24
- 구글 리뷰 크롤링을 통해 리뷰 데이터를 가져와 텍스트로 저장하고,
25
- 프롬프트에 삽입할 리뷰 문자열을 생성하는 클래스.
26
  """
27
- def __init__(self, url, target_review_count=2):
28
  self.url = url
29
  self.target_review_count = target_review_count
30
  self.reviews_text = self.fetch_reviews_text()
@@ -35,13 +34,14 @@ class GoogleReviewManager:
35
  return "(구글 리뷰를 불러오지 못했습니다.)"
36
  reviews = []
37
  for index, row in df_reviews.iterrows():
38
- # 예: [4.5 stars] Excellent service.
39
  reviews.append(f"[{row['Rating']} stars] {row['Review Text']}")
 
40
  return "\n".join(reviews)
41
 
42
  @staticmethod
43
  def format_google_reviews(reviews_text):
44
- # 각 줄로 분리한 "####"가 없는 순수 리뷰만 선택
45
  reviews = [line for line in reviews_text.split("\n") if line.strip() and "####" not in line]
46
  formatted_reviews = []
47
  for i, review in enumerate(reviews, start=1):
@@ -55,61 +55,116 @@ class GoogleReviewManager:
55
  options.add_argument("--headless=new")
56
  options.add_argument("--disable-gpu")
57
  options.add_argument("--window-size=600,600")
 
58
  options.add_argument("--lang=en")
59
- options.add_argument("user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36")
 
60
  driver = webdriver.Chrome(service=service, options=options)
61
  print("웹 드라이버 설정 완료 (헤드리스 모드).")
62
  except Exception as e:
63
  print(f"웹 드라이버 설정 중 오류 발생: {e}")
64
- return pd.DataFrame()
65
 
66
  reviews_data = []
67
- processed_keys = set()
 
68
  try:
69
  driver.get(url)
 
70
  time.sleep(3)
71
- driver.execute_script("document.body.style.zoom = '0.7'")
72
- # 리뷰 탭 버튼 클릭
 
 
 
 
 
 
 
 
 
 
 
 
73
  review_tab_button = None
74
  possible_review_selectors = [
 
75
  (By.XPATH, "//button[contains(text(), 'Reviews')]"),
76
  (By.CSS_SELECTOR, "button[aria-label*='Reviews']"),
77
  ]
78
- wait = WebDriverWait(driver, 10)
79
  for selector in possible_review_selectors:
80
  try:
81
- review_tab_button = wait.until(EC.element_to_be_clickable(selector))
 
 
 
82
  break
83
  except TimeoutException:
84
  continue
85
- if review_tab_button:
86
- review_tab_button.click()
 
 
87
  time.sleep(3)
88
 
89
- # 최신순 정렬 (선택사항)
90
  try:
91
- sort_button = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[contains(@aria-label, 'Sort')]")))
 
 
 
 
 
92
  sort_button.click()
93
  time.sleep(1)
94
- newest_option = wait.until(EC.element_to_be_clickable((By.XPATH, "//div[@role='menuitemradio'][contains(., 'Newest')]")))
 
 
 
 
95
  newest_option.click()
 
96
  time.sleep(3)
97
- except Exception as e:
98
- print(f"정렬 설정 오류: {e}")
 
99
 
100
- scrollable_div = None
 
101
  try:
102
- scrollable_div = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "div.m6QErb.DxyBCb.kA9KIf.dS8AEf.XiKgde[tabindex='-1']")))
 
 
 
103
  except TimeoutException:
104
- print("리뷰 스크롤 영역을 찾지 못했습니다.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
- review_elements_selector = (By.CSS_SELECTOR, "div.jftiEf.fontBodyMedium")
107
  loop_count = 0
108
  max_loop = 50
109
  while len(reviews_data) < TARGET_REVIEW_COUNT and loop_count < max_loop:
110
  loop_count += 1
111
- prev_count = len(reviews_data)
112
  all_reviews = driver.find_elements(*review_elements_selector)
 
 
113
  for review in all_reviews:
114
  try:
115
  reviewer_name = review.find_element(By.CSS_SELECTOR, "div.d4r55").text
@@ -123,44 +178,69 @@ class GoogleReviewManager:
123
  if unique_key in processed_keys:
124
  continue
125
  processed_keys.add(unique_key)
126
- try:
127
- # 클릭하여 전체 텍스트 표시 (더보기)
128
- more_button = review.find_element(By.CSS_SELECTOR, "button.w8nwRe.kyuRq")
129
- driver.execute_script("arguments[0].click();", more_button)
130
- time.sleep(0.3)
131
- except Exception:
132
- pass
133
- try:
134
- review_text = review.find_element(By.CSS_SELECTOR, "span.wiI7pd").text.strip()
135
- except Exception:
136
- review_text = ""
137
  if review_text:
138
  try:
139
- rating = review.find_element(By.CSS_SELECTOR, "span.kvMYJc").get_attribute("aria-label")
 
140
  except Exception:
141
  rating = "N/A"
142
- reviews_data.append({"Rating": rating, "Review Text": review_text})
 
 
 
 
 
 
 
143
  if len(reviews_data) >= TARGET_REVIEW_COUNT:
144
  break
145
- if len(reviews_data) == prev_count:
 
 
146
  break
 
147
  if scrollable_div:
148
- for _ in range(20):
149
- driver.execute_script('arguments[0].scrollBy(0, 1000);', scrollable_div)
150
  time.sleep(0.1)
 
 
151
  time.sleep(2)
 
 
 
152
  if reviews_data:
153
- df = pd.DataFrame(reviews_data[:TARGET_REVIEW_COUNT])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  else:
155
- df = pd.DataFrame()
156
  except Exception as e:
157
- print(f"리뷰 크롤링오류: {e}")
158
- df = pd.DataFrame()
159
  finally:
160
- driver.quit()
161
- return df
162
 
163
- # --- Config 클래스 (Qwen, 구글 리뷰, 새로운 프롬프트 포함) ---
 
 
164
  class Config:
165
  """애플리케이션 설정 및 상수"""
166
  FOOD_ITEMS = [
@@ -175,7 +255,7 @@ class Config:
175
  ]
176
  # 알리바바 Qwen API 키 (기본값은 빈 문자열)
177
  QWEN_API_KEY = ""
178
- # 새로운 프롬프트 템플릿 (구글 리뷰 분석 포함)
179
  DEFAULT_PROMPT_TEMPLATE = (
180
  "### Persona ###\n"
181
  "You are an expert tip calculation assistant focusing on service quality observed in a video, and you also consider the user's review when evaluating the overall experience.\n\n"
@@ -224,11 +304,17 @@ class Config:
224
  "Analysis: [Step-by-step explanation detailing:\n"
225
  " - How you determined the bill amount;\n"
226
  " - Your reasoning for the service quality classification should incorporate specific observations from the video (as described in the Video Caption), as well as a thorough analysis of the Recent Google Reviews, the user's review, and the user's star rating;\n"
227
- " - How you chose the tip percentage within the guideline range, including the calculation details.]\n"
228
- "Final Tip Percentage: [X]%\n"
229
- "Final Tip Amount: $[Calculated Tip]\n"
230
- "Final Total Bill: $[Subtotal + Tip]"
 
 
 
 
 
231
  )
 
232
  CUSTOM_CSS = """
233
  #food-container {
234
  display: grid;
@@ -248,18 +334,21 @@ class Config:
248
  """
249
 
250
  def __init__(self):
251
- # 이미지 폴더 및 파일 확인
 
 
 
 
 
 
252
  if not os.path.exists("images"):
253
  print("경고: 'images' 폴더를 찾을 수 없습니다. 음식 이미지가 표시되지 않을 수 있습니다.")
254
  for item in self.FOOD_ITEMS:
255
  if not os.path.exists(item["image"]):
256
  print(f"경고: 이미지 파일을 찾을 수 없습니다 - {item['image']}")
257
- # 구글 리뷰 크롤링: 원하는 구글 리뷰 URL을 입력 (아래 예시는 임의 URL)
258
- review_url = "https://www.google.com/maps/place/Wolfgang%E2%80%99s+Steakhouse/data=!3m1!4b1!4m6!3m5!1s0x357ca4778cdd1105:0x27d5ead252b66bfd!8m2!3d37.5244965!4d127.0414635!16s%2Fg%2F11c3pwpp26?hl=en&entry=ttu"
259
- self.google_review_manager = GoogleReviewManager(review_url, target_review_count=2)
260
- self.GOOGLE_REVIEWS = GoogleReviewManager.format_google_reviews(self.google_review_manager.reviews_text)
261
 
262
- # --- ModelClients: 알리바바 Qwen API만 사용 ---
 
263
  class ModelClients:
264
  def __init__(self, config: Config):
265
  self.config = config
@@ -273,7 +362,8 @@ class ModelClients:
273
  with open(video_path, "rb") as video_file:
274
  return base64.b64encode(video_file.read()).decode("utf-8")
275
 
276
- # --- VideoProcessor: 비디오 프레임 추출 및 임시 파일 정리 ---
 
277
  class VideoProcessor:
278
  def extract_video_frames(self, video_path, output_folder=None, fps=1):
279
  if not video_path:
@@ -288,7 +378,7 @@ class VideoProcessor:
288
  frame_paths = []
289
  frame_rate = cap.get(cv2.CAP_PROP_FPS)
290
  if not frame_rate or frame_rate == 0:
291
- print("경고: FPS를 읽을 수 없습니다, 기본값 4 설정합니다.")
292
  frame_rate = 4.0
293
  frame_interval = int(frame_rate / fps) if fps > 0 else 1
294
  if frame_interval <= 0:
@@ -336,7 +426,8 @@ class VideoProcessor:
336
  except OSError as e:
337
  print(f"프레임 폴더 삭제 오류: {e}")
338
 
339
- # --- TipCalculator: 알리바바 Qwen API를 사용한 팁 계산 및 파싱 ---
 
340
  class TipCalculator:
341
  def __init__(self, config: Config, model_clients: ModelClients, video_processor: VideoProcessor):
342
  self.config = config
@@ -350,7 +441,9 @@ class TipCalculator:
350
  tip_amount = 0.0
351
  total_bill = 0.0
352
 
353
- analysis_match = re.search(r"Analysis:\s*(.*?)Final Tip Percentage:", output_text, re.DOTALL | re.IGNORECASE)
 
 
354
  if analysis_match:
355
  analysis = analysis_match.group(1).strip()
356
  else:
@@ -358,7 +451,8 @@ class TipCalculator:
358
  if analysis_match_alt:
359
  analysis = analysis_match_alt.group(1).strip()
360
 
361
- percentage_match = re.search(r"Final Tip Percentage:\s*\*{0,2}(\d+(?:\.\d+)?)%\*{0,2}", output_text,
 
362
  re.DOTALL | re.IGNORECASE)
363
  if percentage_match:
364
  try:
@@ -367,7 +461,8 @@ class TipCalculator:
367
  print(f"경고: Tip Percentage 변환 실패 - {percentage_match.group(1)}")
368
  tip_percentage = 0.0
369
 
370
- tip_match = re.search(r"Final Tip Amount:\s*\$?\s*([0-9.]+)", output_text, re.IGNORECASE)
 
371
  if tip_match:
372
  try:
373
  tip_amount = float(tip_match.group(1))
@@ -377,13 +472,13 @@ class TipCalculator:
377
  else:
378
  print(f"경고: 출력에서 Tip Amount를 찾을 수 없습니다:\n{output_text}")
379
 
380
- total_match = re.search(r"Final Total Bill:\s*\$?\s*([0-9.]+)", output_text, re.IGNORECASE)
 
381
  if total_match:
382
  try:
383
  total_bill = float(total_match.group(1))
384
  except ValueError:
385
  print(f"경고: Total Bill 변환 실패 - {total_match.group(1)}")
386
-
387
  if len(analysis) < 20 and analysis == "Analysis not found.":
388
  analysis = output_text
389
 
@@ -426,28 +521,24 @@ Task 2: Provide a short chronological summary of the entire scene.
426
  if not caption_text.strip():
427
  caption_text = "(No caption from Omni)"
428
  user_review = user_review.strip() if user_review else "(No user review)"
429
- # 새로운 프롬프트 템플릿에 구글 리뷰를 포함하도록 업데이트
430
  if custom_prompt is None:
431
  prompt = self.config.DEFAULT_PROMPT_TEMPLATE.format(
432
  calculated_subtotal=calculated_subtotal,
433
  star_rating=star_rating,
434
- user_review=user_review,
435
- google_reviews=self.config.GOOGLE_REVIEWS
436
  )
437
  else:
438
  try:
439
  prompt = custom_prompt.format(
440
  calculated_subtotal=calculated_subtotal,
441
  star_rating=star_rating,
442
- user_review=user_review,
443
- google_reviews=self.config.GOOGLE_REVIEWS
444
  )
445
  except:
446
  prompt = self.config.DEFAULT_PROMPT_TEMPLATE.format(
447
  calculated_subtotal=calculated_subtotal,
448
  star_rating=star_rating,
449
- user_review=user_review,
450
- google_reviews=self.config.GOOGLE_REVIEWS
451
  )
452
  final_prompt = prompt.replace("{caption_text}", caption_text)
453
  qvq_result = self.model_clients.qwen_client.chat.completions.create(
@@ -486,7 +577,8 @@ Task 2: Provide a short chronological summary of the entire scene.
486
  total_bill_output = f"${total_bill:.2f}"
487
  return analysis_output, tip_output, total_bill_output
488
 
489
- # --- UIHandler: Gradio 인터페이스 이벤트 및 Alibaba API 키 업데이트 처리 ---
 
490
  class UIHandler:
491
  def __init__(self, config: Config, tip_calculator: TipCalculator, video_processor: VideoProcessor):
492
  self.config = config
@@ -494,21 +586,29 @@ class UIHandler:
494
  self.video_processor = video_processor
495
 
496
  def update_subtotal_and_prompt(self, *args):
 
497
  num_food_items = len(self.config.FOOD_ITEMS)
498
  quantities = args[:num_food_items]
499
  star_rating = args[num_food_items]
500
  user_review = args[num_food_items + 1]
 
501
  calculated_subtotal = 0.0
502
  for i in range(num_food_items):
503
  calculated_subtotal += self.config.FOOD_ITEMS[i]['price'] * quantities[i]
 
504
  user_review_text = user_review.strip() if user_review and user_review.strip() else "(No user review provided)"
 
 
 
 
505
  updated_prompt = self.config.DEFAULT_PROMPT_TEMPLATE.format(
506
  calculated_subtotal=calculated_subtotal,
507
  star_rating=star_rating,
508
  user_review=user_review_text,
509
- google_reviews=self.config.GOOGLE_REVIEWS
510
  )
511
  updated_prompt = updated_prompt.replace("{caption_text}", "{{caption_text}}")
 
512
  return calculated_subtotal, updated_prompt
513
 
514
  def compute_tip(self, alibaba_key, video_file_obj, subtotal, star_rating, user_review, custom_prompt_text):
@@ -558,6 +658,7 @@ class UIHandler:
558
  analysis, tip_disp, total_bill_disp, prompt_out, vid_out = self.compute_tip(
559
  alibaba_key, video_file_obj, subtotal, star_rating, review, prompt
560
  )
 
561
  invoice = self.update_invoice_summary(*quantities, tip_disp, total_bill_disp)
562
  return analysis, tip_disp, total_bill_disp, prompt_out, vid_out, invoice
563
 
@@ -593,7 +694,8 @@ class UIHandler:
593
  def process_payment(self, total_bill):
594
  return f"{total_bill} 결제되었습니다."
595
 
596
- # --- App: 모든 컴포넌트를 연결하여 Gradio 인터페이스 실행 ---
 
597
  class App:
598
  def __init__(self):
599
  self.config = Config()
@@ -604,7 +706,8 @@ class App:
604
  self.flask_app = Flask(__name__)
605
 
606
  def create_gradio_blocks(self):
607
- with gr.Blocks(title="Video Tip Calculation Interface", theme=gr.themes.Soft(), css=self.config.CUSTOM_CSS) as interface:
 
608
  gr.Markdown("## Video Tip Calculation Interface (Structured)")
609
  quantity_inputs = []
610
  subtotal_display = gr.Number(label="Subtotal ($)", value=0.0, interactive=False, visible=False)
@@ -663,7 +766,7 @@ class App:
663
  calculated_subtotal=0.0,
664
  star_rating=3,
665
  user_review="(No user review provided)",
666
- google_reviews=self.config.GOOGLE_REVIEWS
667
  ).replace("{caption_text}", "{{caption_text}}")
668
  )
669
  gr.Markdown("### 6. AI Analysis")
@@ -690,7 +793,9 @@ class App:
690
  outputs=order_summary_display
691
  )
692
  compute_inputs = [alibaba_key_input, video_input, subtotal_display, rating_input, review_input, prompt_display] + quantity_inputs
693
- compute_outputs = [analysis_display, tip_display, total_bill_display, prompt_display, video_input, order_summary_display]
 
 
694
  qwen_btn.click(
695
  fn=lambda alibaba_key, vid, sub, rat, rev, prom, *qty: self.ui_handler.auto_tip_and_invoice(
696
  alibaba_key, vid, sub, rat, rev, prom, *qty
@@ -740,6 +845,7 @@ class App:
740
  return "Hello Flask"
741
  self.flask_app.run(host="0.0.0.0", port=5000, debug=True)
742
 
 
743
  if __name__ == "__main__":
744
  app = App()
745
  app.run_gradio()
 
3
  import cv2
4
  import base64
5
  import uuid
 
 
 
6
  from flask import Flask
7
+ from ollama import Client
8
+ import openai
9
  import gradio as gr
10
+ import re
11
+ import pandas as pd
12
  from selenium import webdriver
13
  from selenium.webdriver.common.by import By
14
  from selenium.webdriver.chrome.service import Service
15
  from selenium.webdriver.support.ui import WebDriverWait
16
  from selenium.webdriver.support import expected_conditions as EC
 
17
  from webdriver_manager.chrome import ChromeDriverManager
18
+ from selenium.common.exceptions import NoSuchElementException, TimeoutException
19
+ import time
20
 
 
21
  class GoogleReviewManager:
22
  """
23
+ 구글 리뷰 크롤링을 통해 리뷰 데이터를 한 번만 가져와 텍스트로 저장하고,
24
+ DEFAULT_PROMPT_TEMPLATE에 적용할 리뷰 문자열을 생성하는 클래스.
25
  """
26
+ def __init__(self, url, target_review_count=20):
27
  self.url = url
28
  self.target_review_count = target_review_count
29
  self.reviews_text = self.fetch_reviews_text()
 
34
  return "(구글 리뷰를 불러오지 못했습니다.)"
35
  reviews = []
36
  for index, row in df_reviews.iterrows():
37
+ # 예: [4.5 stars] Excellent service and food.
38
  reviews.append(f"[{row['Rating']} stars] {row['Review Text']}")
39
+ # 각 리뷰를 개행 문자로 구분하여 하나의 문자열로 생성
40
  return "\n".join(reviews)
41
 
42
  @staticmethod
43
  def format_google_reviews(reviews_text):
44
+ # 각 줄로 분리하고, 이미 "####"가 포함된 줄은 제외하여 순수한 리뷰 내용만 남김
45
  reviews = [line for line in reviews_text.split("\n") if line.strip() and "####" not in line]
46
  formatted_reviews = []
47
  for i, review in enumerate(reviews, start=1):
 
55
  options.add_argument("--headless=new")
56
  options.add_argument("--disable-gpu")
57
  options.add_argument("--window-size=600,600")
58
+ # 언어 설정 (한글 리뷰를 원하면 "--lang=ko"로 변경)
59
  options.add_argument("--lang=en")
60
+ options.add_argument(
61
+ "user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36")
62
  driver = webdriver.Chrome(service=service, options=options)
63
  print("웹 드라이버 설정 완료 (헤드리스 모드).")
64
  except Exception as e:
65
  print(f"웹 드라이버 설정 중 오류 발생: {e}")
66
+ exit()
67
 
68
  reviews_data = []
69
+ processed_keys = set() # 중복 리뷰 방지를 위한 고유 키 저장
70
+
71
  try:
72
  driver.get(url)
73
+ print("Google Maps 접속 완료.")
74
  time.sleep(3)
75
+ zoom_level = 0.7
76
+ driver.execute_script(f"document.body.style.zoom = '{zoom_level}'")
77
+
78
+ try:
79
+ overall_rating_selector = (By.CSS_SELECTOR, "div.F7nice span[aria-hidden='true']")
80
+ overall_rating_element = WebDriverWait(driver, 10).until(
81
+ EC.visibility_of_element_located(overall_rating_selector)
82
+ )
83
+ overall_rating_text = overall_rating_element.text
84
+ print(f"총 평점 찾음: {overall_rating_text}")
85
+ except (TimeoutException, NoSuchElementException):
86
+ print("총 평점 요소를 찾지 못했습니다.")
87
+
88
+ # 리뷰 탭으로 이동
89
  review_tab_button = None
90
  possible_review_selectors = [
91
+ (By.XPATH, "//button[@role='tab'][contains(., 'Reviews')]"),
92
  (By.XPATH, "//button[contains(text(), 'Reviews')]"),
93
  (By.CSS_SELECTOR, "button[aria-label*='Reviews']"),
94
  ]
95
+ wait_for_review_tab = WebDriverWait(driver, 10)
96
  for selector in possible_review_selectors:
97
  try:
98
+ review_tab_button = wait_for_review_tab.until(
99
+ EC.element_to_be_clickable(selector)
100
+ )
101
+ print(f"리뷰 탭 버튼 찾음 (방식: {selector[0]}, 값: {selector[1]})")
102
  break
103
  except TimeoutException:
104
  continue
105
+ if not review_tab_button:
106
+ print("리뷰 탭 버튼을 찾을 수 없습니다.")
107
+ raise NoSuchElementException("리뷰 탭 버튼 없음")
108
+ review_tab_button.click()
109
  time.sleep(3)
110
 
111
+ # 정렬 버튼 클릭 후 '최신순' 선택
112
  try:
113
+ sort_button_selector = (By.XPATH,
114
+ "//button[contains(@aria-label, '정렬 기준') or contains(@aria-label, 'Sort by') or .//span[contains(text(), 'Sort')]]")
115
+ sort_button = WebDriverWait(driver, 10).until(
116
+ EC.element_to_be_clickable(sort_button_selector)
117
+ )
118
+ print("정렬 기준 버튼 찾음. 클릭 시도...")
119
  sort_button.click()
120
  time.sleep(1)
121
+ newest_option_selector = (By.XPATH, "//div[@role='menuitemradio'][contains(., 'Newest')]")
122
+ newest_option = WebDriverWait(driver, 10).until(
123
+ EC.element_to_be_clickable(newest_option_selector)
124
+ )
125
+ print("최신순 옵션 찾음. 클릭 시도...")
126
  newest_option.click()
127
+ print("최신순으로 정렬 적용됨. 잠시 대기...")
128
  time.sleep(3)
129
+ except (TimeoutException, NoSuchElementException) as e:
130
+ print(f"정렬 적용 오류 발생: {e}. 기본 정렬 상태로 진행합니다.")
131
+ time.sleep(3)
132
 
133
+ scrollable_div_selector = (By.CSS_SELECTOR, "div.m6QErb.DxyBCb.kA9KIf.dS8AEf.XiKgde[tabindex='-1']")
134
+ review_elements_selector = (By.CSS_SELECTOR, "div.jftiEf.fontBodyMedium")
135
  try:
136
+ scrollable_div = WebDriverWait(driver, 15).until(
137
+ EC.presence_of_element_located(scrollable_div_selector)
138
+ )
139
+ print("리뷰 스크롤 영역 찾음.")
140
  except TimeoutException:
141
+ print("리뷰 스크롤 영역을 찾을 수 없습니다.")
142
+ scrollable_div = None
143
+ time.sleep(5)
144
+
145
+ def get_review_text(review):
146
+ try:
147
+ more_button = review.find_element(By.CSS_SELECTOR, "button.w8nwRe.kyuRq")
148
+ driver.execute_script("arguments[0].scrollIntoView(true);", more_button)
149
+ time.sleep(0.3)
150
+ driver.execute_script("arguments[0].click();", more_button)
151
+ time.sleep(0.5)
152
+ except Exception:
153
+ pass
154
+ try:
155
+ review_text = review.find_element(By.CSS_SELECTOR, "span.wiI7pd").text
156
+ return review_text.strip()
157
+ except Exception:
158
+ return ""
159
 
 
160
  loop_count = 0
161
  max_loop = 50
162
  while len(reviews_data) < TARGET_REVIEW_COUNT and loop_count < max_loop:
163
  loop_count += 1
164
+ previous_count = len(reviews_data)
165
  all_reviews = driver.find_elements(*review_elements_selector)
166
+ print(f"Loop {loop_count}: 총 {len(all_reviews)}개의 리뷰 요소 발견.")
167
+
168
  for review in all_reviews:
169
  try:
170
  reviewer_name = review.find_element(By.CSS_SELECTOR, "div.d4r55").text
 
178
  if unique_key in processed_keys:
179
  continue
180
  processed_keys.add(unique_key)
181
+
182
+ review_text = get_review_text(review)
 
 
 
 
 
 
 
 
 
183
  if review_text:
184
  try:
185
+ rating_span = review.find_element(By.CSS_SELECTOR, "span.kvMYJc")
186
+ rating = rating_span.get_attribute("aria-label")
187
  except Exception:
188
  rating = "N/A"
189
+ review_info = {
190
+ "reviewer_name": reviewer_name,
191
+ "rating": rating,
192
+ "date": review_date,
193
+ "text": review_text.replace('\n', ' ')
194
+ }
195
+ reviews_data.append(review_info)
196
+ print(f"리뷰 추가: {reviewer_name}, {review_date}")
197
  if len(reviews_data) >= TARGET_REVIEW_COUNT:
198
  break
199
+
200
+ if len(reviews_data) == previous_count:
201
+ print("새로운 리뷰가 추가되지 않아 스크롤을 중단합니다.")
202
  break
203
+
204
  if scrollable_div:
205
+ for i in range(20):
 
206
  time.sleep(0.1)
207
+ scroll_amount = 1000
208
+ driver.execute_script('arguments[0].scrollBy(0, arguments[1]);', scrollable_div, scroll_amount)
209
  time.sleep(2)
210
+ else:
211
+ break
212
+
213
  if reviews_data:
214
+ review_list = []
215
+ for review in reviews_data[:TARGET_REVIEW_COUNT]:
216
+ rating_str = review['rating']
217
+ if rating_str != "N/A":
218
+ try:
219
+ rating_num = float(rating_str.split()[0])
220
+ except Exception:
221
+ rating_num = None
222
+ else:
223
+ rating_num = None
224
+
225
+ review_list.append({
226
+ "Name": review['reviewer_name'],
227
+ "Rating": rating_num,
228
+ "Date / Time Ago": review['date'],
229
+ "Review Text": review['text']
230
+ })
231
+ df_reviews = pd.DataFrame(review_list)
232
  else:
233
+ df_reviews = pd.DataFrame()
234
  except Exception as e:
235
+ print(f"스크립트 실행예기치 않은 오류 발생: {e}")
236
+ df_reviews = pd.DataFrame()
237
  finally:
238
+ if 'driver' in locals() and driver:
239
+ driver.quit()
240
 
241
+ return df_reviews
242
+
243
+ # --- Config 클래스 (Gemma, GPT4o 제거, Qwen만 사용) ---
244
  class Config:
245
  """애플리케이션 설정 및 상수"""
246
  FOOD_ITEMS = [
 
255
  ]
256
  # 알리바바 Qwen API 키 (기본값은 빈 문자열)
257
  QWEN_API_KEY = ""
258
+
259
  DEFAULT_PROMPT_TEMPLATE = (
260
  "### Persona ###\n"
261
  "You are an expert tip calculation assistant focusing on service quality observed in a video, and you also consider the user's review when evaluating the overall experience.\n\n"
 
304
  "Analysis: [Step-by-step explanation detailing:\n"
305
  " - How you determined the bill amount;\n"
306
  " - Your reasoning for the service quality classification should incorporate specific observations from the video (as described in the Video Caption), as well as a thorough analysis of the Recent Google Reviews, the user's review, and the user's star rating;\n"
307
+ " - How you chose the tip percentage within the guideline range, including the calculation details.]\n\n"
308
+ "### Example Output Indicators(Only Example) ###\n"
309
+ "**Final Tip Percentage**: 2.0%\n"
310
+ "**Final Tip Amount**: $0.50\n"
311
+ "**Final Total Bill**: $25.50\n\n"
312
+ "### Output Indicators ###\n"
313
+ "**Final Tip Percentage**: [X]% (only floating point)\n"
314
+ "**Final Tip Amount**: $[Calculated Tip]\n"
315
+ "**Final Total Bill**: $[Subtotal + Tip]\n"
316
  )
317
+
318
  CUSTOM_CSS = """
319
  #food-container {
320
  display: grid;
 
334
  """
335
 
336
  def __init__(self):
337
+ review_url = "https://www.google.com/maps/place/Wolfgang%E2%80%99s+Steakhouse/data=!3m1!4b1!4m6!3m5!1s0x357ca4778cdd1105:0x27d5ead252b66bfd!8m2!3d37.5244965!4d127.0414635!16s%2Fg%2F11c3pwpp26?hl=en&entry=ttu&g_ep=EgoyMDI1MDQwMi4xIKXMDSoASAFQAw%3D%3D"
338
+ self.google_review_manager = GoogleReviewManager(review_url, target_review_count=2)
339
+ # 여기서 정적 메서드를 통해 리뷰를 미리 포맷하여 저장합니다.
340
+ self.GOOGLE_REVIEWS = GoogleReviewManager.format_google_reviews(
341
+ self.google_review_manager.reviews_text
342
+ )
343
+ # 이미지 디렉토리 확인
344
  if not os.path.exists("images"):
345
  print("경고: 'images' 폴더를 찾을 수 없습니다. 음식 이미지가 표시되지 않을 수 있습니다.")
346
  for item in self.FOOD_ITEMS:
347
  if not os.path.exists(item["image"]):
348
  print(f"경고: 이미지 파일을 찾을 수 없습니다 - {item['image']}")
 
 
 
 
349
 
350
+
351
+ # --- ModelClients (알리바바 Qwen API만 사용) ---
352
  class ModelClients:
353
  def __init__(self, config: Config):
354
  self.config = config
 
362
  with open(video_path, "rb") as video_file:
363
  return base64.b64encode(video_file.read()).decode("utf-8")
364
 
365
+
366
+ # --- VideoProcessor: 비디오 프레임 추출 ---
367
  class VideoProcessor:
368
  def extract_video_frames(self, video_path, output_folder=None, fps=1):
369
  if not video_path:
 
378
  frame_paths = []
379
  frame_rate = cap.get(cv2.CAP_PROP_FPS)
380
  if not frame_rate or frame_rate == 0:
381
+ print("경고: FPS를 읽을 수 없습니다, 기본값 4으로 설정합니다.")
382
  frame_rate = 4.0
383
  frame_interval = int(frame_rate / fps) if fps > 0 else 1
384
  if frame_interval <= 0:
 
426
  except OSError as e:
427
  print(f"프레임 폴더 삭제 오류: {e}")
428
 
429
+
430
+ # --- TipCalculator (알리바바 Qwen API를 사용한 팁 계산) ---
431
  class TipCalculator:
432
  def __init__(self, config: Config, model_clients: ModelClients, video_processor: VideoProcessor):
433
  self.config = config
 
441
  tip_amount = 0.0
442
  total_bill = 0.0
443
 
444
+ # Analysis 부분: "Analysis:" 이후부터 "**Final Tip Percentage**" 이전까지 추출
445
+ analysis_match = re.search(r"Analysis:\s*(.*?)\*\*Final Tip Percentage\*\*", output_text,
446
+ re.DOTALL | re.IGNORECASE)
447
  if analysis_match:
448
  analysis = analysis_match.group(1).strip()
449
  else:
 
451
  if analysis_match_alt:
452
  analysis = analysis_match_alt.group(1).strip()
453
 
454
+ # **Final Tip Percentage** 추출 (예: **Final Tip Percentage**: 2.00%)
455
+ percentage_match = re.search(r"\*\*Final Tip Percentage\*\*:\s*([0-9]+(?:\.[0-9]+)?)%", output_text,
456
  re.DOTALL | re.IGNORECASE)
457
  if percentage_match:
458
  try:
 
461
  print(f"경고: Tip Percentage 변환 실패 - {percentage_match.group(1)}")
462
  tip_percentage = 0.0
463
 
464
+ # **Final Tip Amount** 추출 (예: **Final Tip Amount**: $1.44)
465
+ tip_match = re.search(r"\*\*Final Tip Amount\*\*:\s*\$?\s*([0-9]+(?:\.[0-9]+)?)", output_text, re.IGNORECASE)
466
  if tip_match:
467
  try:
468
  tip_amount = float(tip_match.group(1))
 
472
  else:
473
  print(f"경고: 출력에서 Tip Amount를 찾을 수 없습니다:\n{output_text}")
474
 
475
+ # **Final Total Bill** 추출 (예: **Final Total Bill**: $73.44)
476
+ total_match = re.search(r"\*\*Final Total Bill\*\*:\s*\$?\s*([0-9]+(?:\.[0-9]+)?)", output_text, re.IGNORECASE)
477
  if total_match:
478
  try:
479
  total_bill = float(total_match.group(1))
480
  except ValueError:
481
  print(f"경고: Total Bill 변환 실패 - {total_match.group(1)}")
 
482
  if len(analysis) < 20 and analysis == "Analysis not found.":
483
  analysis = output_text
484
 
 
521
  if not caption_text.strip():
522
  caption_text = "(No caption from Omni)"
523
  user_review = user_review.strip() if user_review else "(No user review)"
 
524
  if custom_prompt is None:
525
  prompt = self.config.DEFAULT_PROMPT_TEMPLATE.format(
526
  calculated_subtotal=calculated_subtotal,
527
  star_rating=star_rating,
528
+ user_review=user_review
 
529
  )
530
  else:
531
  try:
532
  prompt = custom_prompt.format(
533
  calculated_subtotal=calculated_subtotal,
534
  star_rating=star_rating,
535
+ user_review=user_review
 
536
  )
537
  except:
538
  prompt = self.config.DEFAULT_PROMPT_TEMPLATE.format(
539
  calculated_subtotal=calculated_subtotal,
540
  star_rating=star_rating,
541
+ user_review=user_review
 
542
  )
543
  final_prompt = prompt.replace("{caption_text}", caption_text)
544
  qvq_result = self.model_clients.qwen_client.chat.completions.create(
 
577
  total_bill_output = f"${total_bill:.2f}"
578
  return analysis_output, tip_output, total_bill_output
579
 
580
+
581
+ # --- UIHandler: Gradio 인터페이스 이벤트 처리 (알리바바 API 키 입력 포함) ---
582
  class UIHandler:
583
  def __init__(self, config: Config, tip_calculator: TipCalculator, video_processor: VideoProcessor):
584
  self.config = config
 
586
  self.video_processor = video_processor
587
 
588
  def update_subtotal_and_prompt(self, *args):
589
+ """사용자 입력에 따라 소계 및 프롬프트 업데이트"""
590
  num_food_items = len(self.config.FOOD_ITEMS)
591
  quantities = args[:num_food_items]
592
  star_rating = args[num_food_items]
593
  user_review = args[num_food_items + 1]
594
+
595
  calculated_subtotal = 0.0
596
  for i in range(num_food_items):
597
  calculated_subtotal += self.config.FOOD_ITEMS[i]['price'] * quantities[i]
598
+
599
  user_review_text = user_review.strip() if user_review and user_review.strip() else "(No user review provided)"
600
+
601
+ # Google 리뷰 텍스트를 동적으로 포맷팅 (정적 메서드 호출)
602
+ formatted_google_reviews = GoogleReviewManager.format_google_reviews(self.config.GOOGLE_REVIEWS)
603
+ # print(formatted_google_reviews)
604
  updated_prompt = self.config.DEFAULT_PROMPT_TEMPLATE.format(
605
  calculated_subtotal=calculated_subtotal,
606
  star_rating=star_rating,
607
  user_review=user_review_text,
608
+ google_reviews=formatted_google_reviews
609
  )
610
  updated_prompt = updated_prompt.replace("{caption_text}", "{{caption_text}}")
611
+
612
  return calculated_subtotal, updated_prompt
613
 
614
  def compute_tip(self, alibaba_key, video_file_obj, subtotal, star_rating, user_review, custom_prompt_text):
 
658
  analysis, tip_disp, total_bill_disp, prompt_out, vid_out = self.compute_tip(
659
  alibaba_key, video_file_obj, subtotal, star_rating, review, prompt
660
  )
661
+ #print(analysis, tip_disp, total_bill_disp, prompt_out, vid_out)
662
  invoice = self.update_invoice_summary(*quantities, tip_disp, total_bill_disp)
663
  return analysis, tip_disp, total_bill_disp, prompt_out, vid_out, invoice
664
 
 
694
  def process_payment(self, total_bill):
695
  return f"{total_bill} 결제되었습니다."
696
 
697
+
698
+ # --- App: 모든 컴포넌트 연결 및 Gradio 인터페이스 실행 ---
699
  class App:
700
  def __init__(self):
701
  self.config = Config()
 
706
  self.flask_app = Flask(__name__)
707
 
708
  def create_gradio_blocks(self):
709
+ with gr.Blocks(title="Video Tip Calculation Interface", theme=gr.themes.Soft(),
710
+ css=self.config.CUSTOM_CSS) as interface:
711
  gr.Markdown("## Video Tip Calculation Interface (Structured)")
712
  quantity_inputs = []
713
  subtotal_display = gr.Number(label="Subtotal ($)", value=0.0, interactive=False, visible=False)
 
766
  calculated_subtotal=0.0,
767
  star_rating=3,
768
  user_review="(No user review provided)",
769
+ google_reviews=GoogleReviewManager.format_google_reviews(self.config.GOOGLE_REVIEWS)
770
  ).replace("{caption_text}", "{{caption_text}}")
771
  )
772
  gr.Markdown("### 6. AI Analysis")
 
793
  outputs=order_summary_display
794
  )
795
  compute_inputs = [alibaba_key_input, video_input, subtotal_display, rating_input, review_input, prompt_display] + quantity_inputs
796
+ compute_outputs = [
797
+ analysis_display, tip_display, total_bill_display, prompt_display, video_input, order_summary_display
798
+ ]
799
  qwen_btn.click(
800
  fn=lambda alibaba_key, vid, sub, rat, rev, prom, *qty: self.ui_handler.auto_tip_and_invoice(
801
  alibaba_key, vid, sub, rat, rev, prom, *qty
 
845
  return "Hello Flask"
846
  self.flask_app.run(host="0.0.0.0", port=5000, debug=True)
847
 
848
+
849
  if __name__ == "__main__":
850
  app = App()
851
  app.run_gradio()
requirements.txt CHANGED
@@ -2,3 +2,5 @@ gradio
2
  opencv-python
3
  flask
4
  openai
 
 
 
2
  opencv-python
3
  flask
4
  openai
5
+ webdriver-manager
6
+ selenium