kfkas commited on
Commit
2b8f7ee
·
1 Parent(s): 80a4e5a
Files changed (3) hide show
  1. .gitattributes +0 -35
  2. app.py +229 -49
  3. space.yaml +0 -4
.gitattributes CHANGED
@@ -1,37 +1,2 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  *.task filter=lfs diff=lfs merge=lfs -text
37
  *.mp4 filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  *.task filter=lfs diff=lfs merge=lfs -text
2
  *.mp4 filter=lfs diff=lfs merge=lfs -text
app.py CHANGED
@@ -4,10 +4,163 @@ import cv2
4
  import base64
5
  import uuid
6
  import re
 
 
7
  from flask import Flask
8
  import gradio as gr
9
 
10
- # --- Config 클래스 (Gemma, GPT4o 제거, Qwen만 사용) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  class Config:
12
  """애플리케이션 설정 및 상수"""
13
  FOOD_ITEMS = [
@@ -22,42 +175,60 @@ class Config:
22
  ]
23
  # 알리바바 Qwen API 키 (기본값은 빈 문자열)
24
  QWEN_API_KEY = ""
25
-
26
  DEFAULT_PROMPT_TEMPLATE = (
27
  "### Persona ###\n"
28
- "You are an expert tip calculation assistant focusing on service quality observed in a video.\n\n"
 
29
  "### Task ###\n"
30
- "1. Watch the video frames provided (via caption) and analyze the service provided by the staff. Provide a summary of the observed actions and interactions in the video.\n"
31
- "2. Identify the bill amount. First, look for explicit mentions in the 'Video Caption'. If not found, use the 'Calculated Subtotal' from the context. If neither is available, assume a default value of $50.\n"
32
- "3. Based *only* on the actions and service quality observed in the video (described in the caption) and the user's review/rating, classify the service quality as one of:\n"
33
- " - Poor\n"
34
- " - Average\n"
35
- " - Good\n"
36
- " Explain your reasoning for the classification based on specific observations from the video and the user's feedback.\n"
37
- "4. Use the following tip guidelines based *only* on the classified service quality:\n"
38
- " - Poor Service: 0%~5% of the bill\n"
39
- " - Average Service: 10%~15% of the bill\n"
40
- " - Good Service: 15%~20% of the bill\n"
41
- "5. Calculate a specific tip percentage based on the service quality classification. Choose a percentage within the suggested range.\n"
42
- "6. Calculate the tip amount by multiplying the bill amount by the chosen percentage. Round to two decimal places.\n"
43
- "7. Calculate the total bill by adding the tip amount to the subtotal. Round to two decimal places.\n\n"
44
- "### Context ###\n"
 
 
 
 
 
 
 
45
  " - Current Country: USA\n"
46
  " - Restaurant Name: The Golden Spoon (Assumed)\n"
47
  " - Calculated Subtotal: ${calculated_subtotal:.2f}\n"
48
  " - User Star Rating: {star_rating} / 5\n"
49
  " - Currently User Review: {user_review}\n\n"
 
 
 
 
 
 
50
  "### Input ###\n"
51
  "Video Caption:\n{{caption_text}}\n\n"
 
52
  "### Output ###\n"
53
  "Return your answer in the exact format below:\n"
54
  "Video Text Analysis: [Summary of the observed actions and interactions of the staff in the video.]\n"
55
- "Analysis: [Explain step-by-step: How you determined the bill amount used. Your detailed reasoning for the service quality classification based on *specific observations from the video (as described in the Video Caption)* and the user's review/rating. How you chose the tip percentage within the guideline range. Show the calculation with the exact percentage used.]\n"
56
- "Tip Percentage: [X]%\n"
57
- "Tip Amount: $[Calculated Tip]\n"
58
- "Total Bill: $[Subtotal + Tip]"
 
 
 
 
59
  )
60
-
61
  CUSTOM_CSS = """
62
  #food-container {
63
  display: grid;
@@ -77,14 +248,18 @@ class Config:
77
  """
78
 
79
  def __init__(self):
 
80
  if not os.path.exists("images"):
81
  print("경고: 'images' 폴더를 찾을 수 없습니다. 음식 이미지가 표시되지 않을 수 있습니다.")
82
  for item in self.FOOD_ITEMS:
83
  if not os.path.exists(item["image"]):
84
  print(f"경고: 이미지 파일을 찾을 수 없습니다 - {item['image']}")
 
 
 
 
85
 
86
-
87
- # --- ModelClients (알리바바 Qwen API만 사용) ---
88
  class ModelClients:
89
  def __init__(self, config: Config):
90
  self.config = config
@@ -98,8 +273,7 @@ class ModelClients:
98
  with open(video_path, "rb") as video_file:
99
  return base64.b64encode(video_file.read()).decode("utf-8")
100
 
101
-
102
- # --- VideoProcessor: 비디오 프레임 추출 ---
103
  class VideoProcessor:
104
  def extract_video_frames(self, video_path, output_folder=None, fps=1):
105
  if not video_path:
@@ -114,7 +288,7 @@ class VideoProcessor:
114
  frame_paths = []
115
  frame_rate = cap.get(cv2.CAP_PROP_FPS)
116
  if not frame_rate or frame_rate == 0:
117
- print("경고: FPS를 읽을 수 없습니다, 기본값 4으로 설정합니다.")
118
  frame_rate = 4.0
119
  frame_interval = int(frame_rate / fps) if fps > 0 else 1
120
  if frame_interval <= 0:
@@ -162,8 +336,7 @@ class VideoProcessor:
162
  except OSError as e:
163
  print(f"프레임 폴더 삭제 오류: {e}")
164
 
165
-
166
- # --- TipCalculator (알리바바 Qwen API를 사용한 팁 계산) ---
167
  class TipCalculator:
168
  def __init__(self, config: Config, model_clients: ModelClients, video_processor: VideoProcessor):
169
  self.config = config
@@ -171,18 +344,21 @@ class TipCalculator:
171
  self.video_processor = video_processor
172
 
173
  def parse_llm_output(self, output_text):
 
174
  analysis = "Analysis not found."
175
  tip_percentage = 0.0
176
  tip_amount = 0.0
177
  total_bill = 0.0
178
- analysis_match = re.search(r"Analysis:\s*(.*?)Tip Percentage:", output_text, re.DOTALL | re.IGNORECASE)
 
179
  if analysis_match:
180
  analysis = analysis_match.group(1).strip()
181
  else:
182
  analysis_match_alt = re.search(r"Analysis:\s*(.*)", output_text, re.DOTALL | re.IGNORECASE)
183
  if analysis_match_alt:
184
  analysis = analysis_match_alt.group(1).strip()
185
- percentage_match = re.search(r"Tip Percentage:\s*\*{0,2}(\d+(?:\.\d+)?)%\*{0,2}", output_text,
 
186
  re.DOTALL | re.IGNORECASE)
187
  if percentage_match:
188
  try:
@@ -190,7 +366,8 @@ class TipCalculator:
190
  except ValueError:
191
  print(f"경고: Tip Percentage 변환 실패 - {percentage_match.group(1)}")
192
  tip_percentage = 0.0
193
- tip_match = re.search(r"Tip Amount:\s*\$?\s*([0-9.]+)", output_text, re.IGNORECASE)
 
194
  if tip_match:
195
  try:
196
  tip_amount = float(tip_match.group(1))
@@ -199,14 +376,17 @@ class TipCalculator:
199
  tip_amount = 0.0
200
  else:
201
  print(f"경고: 출력에서 Tip Amount를 찾을 수 없습니다:\n{output_text}")
202
- total_match = re.search(r"Total Bill:\s*\$?\s*([0-9.]+)", output_text, re.IGNORECASE)
 
203
  if total_match:
204
  try:
205
  total_bill = float(total_match.group(1))
206
  except ValueError:
207
  print(f"경고: Total Bill 변환 실패 - {total_match.group(1)}")
 
208
  if len(analysis) < 20 and analysis == "Analysis not found.":
209
  analysis = output_text
 
210
  return analysis, tip_percentage, tip_amount, output_text
211
 
212
  def process_tip_qwen(self, video_file_path, star_rating, user_review, calculated_subtotal, custom_prompt=None):
@@ -246,24 +426,28 @@ Task 2: Provide a short chronological summary of the entire scene.
246
  if not caption_text.strip():
247
  caption_text = "(No caption from Omni)"
248
  user_review = user_review.strip() if user_review else "(No user review)"
 
249
  if custom_prompt is None:
250
  prompt = self.config.DEFAULT_PROMPT_TEMPLATE.format(
251
  calculated_subtotal=calculated_subtotal,
252
  star_rating=star_rating,
253
- user_review=user_review
 
254
  )
255
  else:
256
  try:
257
  prompt = custom_prompt.format(
258
  calculated_subtotal=calculated_subtotal,
259
  star_rating=star_rating,
260
- user_review=user_review
 
261
  )
262
  except:
263
  prompt = self.config.DEFAULT_PROMPT_TEMPLATE.format(
264
  calculated_subtotal=calculated_subtotal,
265
  star_rating=star_rating,
266
- user_review=user_review
 
267
  )
268
  final_prompt = prompt.replace("{caption_text}", caption_text)
269
  qvq_result = self.model_clients.qwen_client.chat.completions.create(
@@ -302,8 +486,7 @@ Task 2: Provide a short chronological summary of the entire scene.
302
  total_bill_output = f"${total_bill:.2f}"
303
  return analysis_output, tip_output, total_bill_output
304
 
305
-
306
- # --- UIHandler: Gradio 인터페이스 이벤트 처리 (알리바바 API 키 입력 포함) ---
307
  class UIHandler:
308
  def __init__(self, config: Config, tip_calculator: TipCalculator, video_processor: VideoProcessor):
309
  self.config = config
@@ -322,7 +505,8 @@ class UIHandler:
322
  updated_prompt = self.config.DEFAULT_PROMPT_TEMPLATE.format(
323
  calculated_subtotal=calculated_subtotal,
324
  star_rating=star_rating,
325
- user_review=user_review_text
 
326
  )
327
  updated_prompt = updated_prompt.replace("{caption_text}", "{{caption_text}}")
328
  return calculated_subtotal, updated_prompt
@@ -409,8 +593,7 @@ class UIHandler:
409
  def process_payment(self, total_bill):
410
  return f"{total_bill} 결제되었습니다."
411
 
412
-
413
- # --- App: 모든 컴포넌트 연결 및 Gradio 인터페이스 실행 ---
414
  class App:
415
  def __init__(self):
416
  self.config = Config()
@@ -421,8 +604,7 @@ class App:
421
  self.flask_app = Flask(__name__)
422
 
423
  def create_gradio_blocks(self):
424
- with gr.Blocks(title="Video Tip Calculation Interface", theme=gr.themes.Soft(),
425
- css=self.config.CUSTOM_CSS) as interface:
426
  gr.Markdown("## Video Tip Calculation Interface (Structured)")
427
  quantity_inputs = []
428
  subtotal_display = gr.Number(label="Subtotal ($)", value=0.0, interactive=False, visible=False)
@@ -480,7 +662,8 @@ class App:
480
  value=self.config.DEFAULT_PROMPT_TEMPLATE.format(
481
  calculated_subtotal=0.0,
482
  star_rating=3,
483
- user_review="(No user review provided)"
 
484
  ).replace("{caption_text}", "{{caption_text}}")
485
  )
486
  gr.Markdown("### 6. AI Analysis")
@@ -507,9 +690,7 @@ class App:
507
  outputs=order_summary_display
508
  )
509
  compute_inputs = [alibaba_key_input, video_input, subtotal_display, rating_input, review_input, prompt_display] + quantity_inputs
510
- compute_outputs = [
511
- analysis_display, tip_display, total_bill_display, prompt_display, video_input, order_summary_display
512
- ]
513
  qwen_btn.click(
514
  fn=lambda alibaba_key, vid, sub, rat, rev, prom, *qty: self.ui_handler.auto_tip_and_invoice(
515
  alibaba_key, vid, sub, rat, rev, prom, *qty
@@ -559,7 +740,6 @@ class App:
559
  return "Hello Flask"
560
  self.flask_app.run(host="0.0.0.0", port=5000, debug=True)
561
 
562
-
563
  if __name__ == "__main__":
564
  app = App()
565
  app.run_gradio()
 
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()
31
+
32
+ def fetch_reviews_text(self):
33
+ df_reviews = self.google_review_crawling(self.target_review_count, self.url)
34
+ if df_reviews.empty:
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):
48
+ formatted_reviews.append(f"#### Google Review {i} ####\n{review}")
49
+ return "\n\n".join(formatted_reviews)
50
+
51
+ def google_review_crawling(self, TARGET_REVIEW_COUNT, url):
52
+ try:
53
+ service = Service(ChromeDriverManager().install())
54
+ options = webdriver.ChromeOptions()
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
116
+ except Exception:
117
+ reviewer_name = "N/A"
118
+ try:
119
+ review_date = review.find_element(By.CSS_SELECTOR, "span.rsqaWe").text
120
+ except Exception:
121
+ review_date = "N/A"
122
+ unique_key = reviewer_name + review_date
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
  ]
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"
182
+
183
  "### Task ###\n"
184
+ "1. **Video Analysis**: Watch the video frames provided (via caption) and analyze the service provided by the staff. Provide a concise summary of the observed actions and interactions in the video.\n\n"
185
+ "2. **Bill Amount Determination**: Identify the bill amount by first looking for explicit mentions in the 'Video Caption'. If not found, use the 'Calculated Subtotal' from the context. If neither is available, assume a default value of $50.\n\n"
186
+ "3. **Service Quality Classification**:\n"
187
+ " - Based *only* on the actions and service quality observed in the video (as described in the Video Caption) **and** the user's review/rating,\n"
188
+ " classify the service quality as one of the following:\n"
189
+ " - Poor\n"
190
+ " - Average\n"
191
+ " - Good\n"
192
+ " - Explain your reasoning for the classification with specific observations from both the video and the user's review.\n\n"
193
+ "4. **Tip Guidelines and Calculation**:\n"
194
+ " - Apply the following tip guidelines based *only* on the classified service quality:\n"
195
+ " - Poor Service: 0% ~ 5% of the bill\n"
196
+ " - Average Service: 10% ~ 15% of the bill\n"
197
+ " - Good Service: 15% ~ 20% of the bill\n"
198
+ " - Choose a specific tip percentage within the suggested range.\n"
199
+ " - Calculate the tip amount by multiplying the determined bill amount by the chosen percentage (round to two decimal places).\n"
200
+ " - Calculate the total bill by adding the tip amount to the subtotal (round to two decimal places).\n\n"
201
+ "5. **Google Review Consideration**:\n"
202
+ " - If the Recent Google Reviews are predominantly racist, extremely negative, or report bugs (e.g., insects or other quality issues),\n"
203
+ " you must reflect this by applying a significantly lower tip percentage (preferably within the 0% to 5% range) in your calculation.\n\n"
204
+
205
+ "### User Context ###\n"
206
  " - Current Country: USA\n"
207
  " - Restaurant Name: The Golden Spoon (Assumed)\n"
208
  " - Calculated Subtotal: ${calculated_subtotal:.2f}\n"
209
  " - User Star Rating: {star_rating} / 5\n"
210
  " - Currently User Review: {user_review}\n\n"
211
+
212
+ "### Recent Google Review ###\n\n"
213
+ "{google_reviews}\n\n"
214
+ "#### Recent Google Review 3 ####\n"
215
+ "[1.0 stars] It was a very racist remark, 매우 인종차별적인 발언을 겪었어요. 모욕적입니다,,\n\n"
216
+
217
  "### Input ###\n"
218
  "Video Caption:\n{{caption_text}}\n\n"
219
+
220
  "### Output ###\n"
221
  "Return your answer in the exact format below:\n"
222
  "Video Text Analysis: [Summary of the observed actions and interactions of the staff in the video.]\n"
223
+ "Recent Google Review Analysis : [Check if the recent Google Review is predominantly racist, extremely negative, or report bugs (e.g., insects or other quality issues).]\n"
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
  """
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
  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
  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
  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
 
344
  self.video_processor = video_processor
345
 
346
  def parse_llm_output(self, output_text):
347
+ """LLM 출력을 파싱하여 팁 계산 결과 추출"""
348
  analysis = "Analysis not found."
349
  tip_percentage = 0.0
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:
357
  analysis_match_alt = re.search(r"Analysis:\s*(.*)", output_text, re.DOTALL | re.IGNORECASE)
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:
 
366
  except ValueError:
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))
 
376
  tip_amount = 0.0
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
+
390
  return analysis, tip_percentage, tip_amount, output_text
391
 
392
  def process_tip_qwen(self, video_file_path, star_rating, user_review, calculated_subtotal, custom_prompt=None):
 
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
  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
 
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
 
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
  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)
 
662
  value=self.config.DEFAULT_PROMPT_TEMPLATE.format(
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
  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
  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()
space.yaml DELETED
@@ -1,4 +0,0 @@
1
- runtime: python
2
- python_version: "3.10"
3
- hardware:
4
- gpu: false