kfkas commited on
Commit
1829ad4
·
1 Parent(s): edb47bf
Files changed (3) hide show
  1. .gitignore +1 -0
  2. app.py +177 -372
  3. requirements.txt +1 -3
.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ google_app.py
app.py CHANGED
@@ -3,240 +3,9 @@ import shutil
3
  import cv2
4
  import base64
5
  import uuid
 
6
  from flask import Flask
7
  import gradio as gr
8
- import re
9
- import pandas as pd
10
- from selenium import webdriver
11
- from selenium.webdriver.common.by import By
12
- from selenium.webdriver.chrome.service import Service
13
- from selenium.webdriver.support.ui import WebDriverWait
14
- from selenium.webdriver.support import expected_conditions as EC
15
- from webdriver_manager.chrome import ChromeDriverManager
16
- from selenium.common.exceptions import NoSuchElementException, TimeoutException
17
- import time
18
-
19
- class GoogleReviewManager:
20
- """
21
- 구글 리뷰 크롤링을 통해 리뷰 데이터를 한 번만 가져와 텍스트로 저장하고,
22
- DEFAULT_PROMPT_TEMPLATE에 적용할 리뷰 문자열을 생성하는 클래스.
23
- """
24
- def __init__(self, url, target_review_count=20):
25
- self.url = url
26
- self.target_review_count = target_review_count
27
- self.reviews_text = self.fetch_reviews_text()
28
-
29
- def fetch_reviews_text(self):
30
- df_reviews = self.google_review_crawling(self.target_review_count, self.url)
31
- if df_reviews.empty:
32
- return "(구글 리뷰를 불러오지 못했습니다.)"
33
- reviews = []
34
- for index, row in df_reviews.iterrows():
35
- # 예: [4.5 stars] Excellent service and food.
36
- reviews.append(f"[{row['Rating']} stars] {row['Review Text']}")
37
- # 각 리뷰를 개행 문자로 구분하여 하나의 문자열로 생성
38
- return "\n".join(reviews)
39
-
40
- @staticmethod
41
- def format_google_reviews(reviews_text):
42
- # 각 줄로 분리하고, 이미 "####"가 포함된 줄은 제외하여 순수한 리뷰 내용만 남김
43
- reviews = [line for line in reviews_text.split("\n") if line.strip() and "####" not in line]
44
- formatted_reviews = []
45
- for i, review in enumerate(reviews, start=1):
46
- formatted_reviews.append(f"#### Google Review {i} ####\n{review}")
47
- return "\n\n".join(formatted_reviews)
48
-
49
- def google_review_crawling(self, TARGET_REVIEW_COUNT, url):
50
- try:
51
- service = Service(ChromeDriverManager().install())
52
- options = webdriver.ChromeOptions()
53
- options.add_argument("--headless=new")
54
- options.add_argument("--disable-gpu")
55
- options.add_argument("--window-size=600,600")
56
- # 언어 설정 (한글 리뷰를 원하면 "--lang=ko"로 변경)
57
- options.add_argument("--lang=en")
58
- options.add_argument(
59
- "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
- exit()
65
-
66
- reviews_data = []
67
- processed_keys = set() # 중복 리뷰 방지를 위한 고유 키 저장
68
-
69
- try:
70
- driver.get(url)
71
- print("Google Maps 접속 완료.")
72
- time.sleep(3)
73
- zoom_level = 0.7
74
- driver.execute_script(f"document.body.style.zoom = '{zoom_level}'")
75
-
76
- try:
77
- overall_rating_selector = (By.CSS_SELECTOR, "div.F7nice span[aria-hidden='true']")
78
- overall_rating_element = WebDriverWait(driver, 10).until(
79
- EC.visibility_of_element_located(overall_rating_selector)
80
- )
81
- overall_rating_text = overall_rating_element.text
82
- print(f"총 평점 찾음: {overall_rating_text}")
83
- except (TimeoutException, NoSuchElementException):
84
- print("총 평점 요소를 찾지 못했습니다.")
85
-
86
- # 리뷰 탭으로 이동
87
- review_tab_button = None
88
- possible_review_selectors = [
89
- (By.XPATH, "//button[@role='tab'][contains(., 'Reviews')]"),
90
- (By.XPATH, "//button[contains(text(), 'Reviews')]"),
91
- (By.CSS_SELECTOR, "button[aria-label*='Reviews']"),
92
- ]
93
- wait_for_review_tab = WebDriverWait(driver, 10)
94
- for selector in possible_review_selectors:
95
- try:
96
- review_tab_button = wait_for_review_tab.until(
97
- EC.element_to_be_clickable(selector)
98
- )
99
- print(f"리뷰 탭 버튼 찾음 (방식: {selector[0]}, 값: {selector[1]})")
100
- break
101
- except TimeoutException:
102
- continue
103
- if not review_tab_button:
104
- print("리뷰 탭 버튼을 찾을 수 없습니다.")
105
- raise NoSuchElementException("리뷰 탭 버튼 없음")
106
- review_tab_button.click()
107
- time.sleep(3)
108
-
109
- # 정렬 버튼 클릭 후 '최신순' 선택
110
- try:
111
- sort_button_selector = (By.XPATH,
112
- "//button[contains(@aria-label, '정렬 기준') or contains(@aria-label, 'Sort by') or .//span[contains(text(), 'Sort')]]")
113
- sort_button = WebDriverWait(driver, 10).until(
114
- EC.element_to_be_clickable(sort_button_selector)
115
- )
116
- print("정렬 기준 버튼 찾음. 클릭 시도...")
117
- sort_button.click()
118
- time.sleep(1)
119
- newest_option_selector = (By.XPATH, "//div[@role='menuitemradio'][contains(., 'Newest')]")
120
- newest_option = WebDriverWait(driver, 10).until(
121
- EC.element_to_be_clickable(newest_option_selector)
122
- )
123
- print("최신순 옵션 찾음. 클릭 시도...")
124
- newest_option.click()
125
- print("최신순으로 정렬 적용됨. 잠시 대기...")
126
- time.sleep(3)
127
- except (TimeoutException, NoSuchElementException) as e:
128
- print(f"정렬 적용 중 오류 발생: {e}. 기본 정렬 상태로 진행합니다.")
129
- time.sleep(3)
130
-
131
- scrollable_div_selector = (By.CSS_SELECTOR, "div.m6QErb.DxyBCb.kA9KIf.dS8AEf.XiKgde[tabindex='-1']")
132
- review_elements_selector = (By.CSS_SELECTOR, "div.jftiEf.fontBodyMedium")
133
- try:
134
- scrollable_div = WebDriverWait(driver, 15).until(
135
- EC.presence_of_element_located(scrollable_div_selector)
136
- )
137
- print("리뷰 스크롤 영역 찾음.")
138
- except TimeoutException:
139
- print("리뷰 스크롤 영역을 찾을 수 없습니다.")
140
- scrollable_div = None
141
- time.sleep(5)
142
-
143
- def get_review_text(review):
144
- try:
145
- more_button = review.find_element(By.CSS_SELECTOR, "button.w8nwRe.kyuRq")
146
- driver.execute_script("arguments[0].scrollIntoView(true);", more_button)
147
- time.sleep(0.3)
148
- driver.execute_script("arguments[0].click();", more_button)
149
- time.sleep(0.5)
150
- except Exception:
151
- pass
152
- try:
153
- review_text = review.find_element(By.CSS_SELECTOR, "span.wiI7pd").text
154
- return review_text.strip()
155
- except Exception:
156
- return ""
157
-
158
- loop_count = 0
159
- max_loop = 50
160
- while len(reviews_data) < TARGET_REVIEW_COUNT and loop_count < max_loop:
161
- loop_count += 1
162
- previous_count = len(reviews_data)
163
- all_reviews = driver.find_elements(*review_elements_selector)
164
- print(f"Loop {loop_count}: 총 {len(all_reviews)}개의 리뷰 요소 발견.")
165
-
166
- for review in all_reviews:
167
- try:
168
- reviewer_name = review.find_element(By.CSS_SELECTOR, "div.d4r55").text
169
- except Exception:
170
- reviewer_name = "N/A"
171
- try:
172
- review_date = review.find_element(By.CSS_SELECTOR, "span.rsqaWe").text
173
- except Exception:
174
- review_date = "N/A"
175
- unique_key = reviewer_name + review_date
176
- if unique_key in processed_keys:
177
- continue
178
- processed_keys.add(unique_key)
179
-
180
- review_text = get_review_text(review)
181
- if review_text:
182
- try:
183
- rating_span = review.find_element(By.CSS_SELECTOR, "span.kvMYJc")
184
- rating = rating_span.get_attribute("aria-label")
185
- except Exception:
186
- rating = "N/A"
187
- review_info = {
188
- "reviewer_name": reviewer_name,
189
- "rating": rating,
190
- "date": review_date,
191
- "text": review_text.replace('\n', ' ')
192
- }
193
- reviews_data.append(review_info)
194
- print(f"리뷰 추가: {reviewer_name}, {review_date}")
195
- if len(reviews_data) >= TARGET_REVIEW_COUNT:
196
- break
197
-
198
- if len(reviews_data) == previous_count:
199
- print("새로운 리뷰가 추가되지 않아 스크롤을 중단합니다.")
200
- break
201
-
202
- if scrollable_div:
203
- for i in range(20):
204
- time.sleep(0.1)
205
- scroll_amount = 1000
206
- driver.execute_script('arguments[0].scrollBy(0, arguments[1]);', scrollable_div, scroll_amount)
207
- time.sleep(2)
208
- else:
209
- break
210
-
211
- if reviews_data:
212
- review_list = []
213
- for review in reviews_data[:TARGET_REVIEW_COUNT]:
214
- rating_str = review['rating']
215
- if rating_str != "N/A":
216
- try:
217
- rating_num = float(rating_str.split()[0])
218
- except Exception:
219
- rating_num = None
220
- else:
221
- rating_num = None
222
-
223
- review_list.append({
224
- "Name": review['reviewer_name'],
225
- "Rating": rating_num,
226
- "Date / Time Ago": review['date'],
227
- "Review Text": review['text']
228
- })
229
- df_reviews = pd.DataFrame(review_list)
230
- else:
231
- df_reviews = pd.DataFrame()
232
- except Exception as e:
233
- print(f"스크립트 실행 중 예기치 않은 오류 발생: {e}")
234
- df_reviews = pd.DataFrame()
235
- finally:
236
- if 'driver' in locals() and driver:
237
- driver.quit()
238
-
239
- return df_reviews
240
 
241
  # --- Config 클래스 (Gemma, GPT4o 제거, Qwen만 사용) ---
242
  class Config:
@@ -271,7 +40,9 @@ class Config:
271
  "3. **Overall Service Quality Evaluation**:\n"
272
  " Evaluate the service quality by evenly scoring the following four components, each out of 100:\n"
273
  " a) **Video Service Score**: Service quality observed in the video (from Video Caption).\n"
274
- " b) **Google Review Score**: Insights from the Recent Google Reviews (note if any racist, extremely negative comments, or quality issues are reported).\n"
 
 
275
  " c) **User Review Score**: The user's review (if the review mentions improvements or enhanced service, especially if it indicates that previously reported issues like racism have been resolved, adjust the negative impact accordingly).\n"
276
  " d) **Star Rating Score**: The user's star rating, interpreted on a scale where 5/5 corresponds to 100 points.\n"
277
  " - Calculate the overall average score by taking the mean of these four scores.\n\n"
@@ -289,6 +60,11 @@ class Config:
289
  " - Even though all four factors are evaluated equally, if the user's review explicitly indicates improvements (e.g., stating 'There's no racism!' or similar phrases), then increase the Google Review score by adjusting or restoring it to a value higher than the score initially reduced due to negative comments. In other words, if improvements are noted, add a positive increment (using a '+' adjustment) to the Google Review score, ensuring it is higher than the negatively impacted value.\n"
290
  " - Ensure that the user's review score takes priority when there is a conflict with the negative tone of the Google Reviews.\n\n"
291
 
 
 
 
 
 
292
  "### User Context ###\n"
293
  " - Current Country: USA\n"
294
  " - Restaurant Name: The Golden Spoon (Assumed)\n"
@@ -297,10 +73,14 @@ class Config:
297
  " - Currently User Review: {user_review}\n\n"
298
 
299
  "### Recent Google Review ###\n\n"
300
- "{google_reviews}\n\n"
301
- "#### Recent Google Review 3 ####\n"
302
- "[1.0 stars] It was a very racist remark, 매우 인종차별적인 발언을 겪었어요. 모욕적입니다,,\n\n"
303
 
 
 
 
 
 
 
 
304
  "### Input ###\n"
305
  "Video Caption:\n{{caption_text}}\n\n"
306
 
@@ -313,10 +93,17 @@ class Config:
313
  "Overall Analysis: [Step-by-step explanation detailing:\n"
314
  " - How the bill amount was determined;\n"
315
  " - How each of the four components was scored and how any negative scores (e.g., for racism) were adjusted based on improvement indications in the user's review;\n"
 
316
  " - How the overall average score was calculated;\n"
317
  " - The reasoning for the final service quality classification based on the average score;\n"
318
- " - How the tip percentage was chosen within the guideline range and the detailed calculation for the tip amount and final total bill.]\n\n"
 
319
 
 
 
 
 
 
320
  "### Example Output Indicators (for reference only) ###\n"
321
  "**Final Tip Percentage**: 12.5%\n"
322
  "**Final Tip Amount**: $6.25\n"
@@ -326,6 +113,9 @@ class Config:
326
  "**Final Tip Percentage**: [X]% (only floating point)\n"
327
  "**Final Tip Amount**: $[Calculated Tip]\n"
328
  "**Final Total Bill**: $[Subtotal + Tip]\n"
 
 
 
329
  )
330
 
331
  CUSTOM_CSS = """
@@ -347,13 +137,6 @@ class Config:
347
  """
348
 
349
  def __init__(self):
350
- 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"
351
- self.google_review_manager = GoogleReviewManager(review_url, target_review_count=2)
352
- # 여기서 정적 메서드를 통해 리뷰를 미리 포맷하여 저장합니다.
353
- self.GOOGLE_REVIEWS = GoogleReviewManager.format_google_reviews(
354
- self.google_review_manager.reviews_text
355
- )
356
- # 이미지 디렉토리 확인
357
  if not os.path.exists("images"):
358
  print("경고: 'images' 폴더를 찾을 수 없습니다. 음식 이미지가 표시되지 않을 수 있습니다.")
359
  for item in self.FOOD_ITEMS:
@@ -599,29 +382,20 @@ class UIHandler:
599
  self.video_processor = video_processor
600
 
601
  def update_subtotal_and_prompt(self, *args):
602
- """사용자 입력에 따라 소계 및 프롬프트 업데이트"""
603
  num_food_items = len(self.config.FOOD_ITEMS)
604
  quantities = args[:num_food_items]
605
  star_rating = args[num_food_items]
606
  user_review = args[num_food_items + 1]
607
-
608
  calculated_subtotal = 0.0
609
  for i in range(num_food_items):
610
  calculated_subtotal += self.config.FOOD_ITEMS[i]['price'] * quantities[i]
611
-
612
  user_review_text = user_review.strip() if user_review and user_review.strip() else "(No user review provided)"
613
-
614
- # Google 리뷰 텍스트를 동적으로 포맷팅 (정적 메서드 호출)
615
- formatted_google_reviews = GoogleReviewManager.format_google_reviews(self.config.GOOGLE_REVIEWS)
616
- # print(formatted_google_reviews)
617
  updated_prompt = self.config.DEFAULT_PROMPT_TEMPLATE.format(
618
  calculated_subtotal=calculated_subtotal,
619
  star_rating=star_rating,
620
- user_review=user_review_text,
621
- google_reviews=formatted_google_reviews
622
  )
623
  updated_prompt = updated_prompt.replace("{caption_text}", "{{caption_text}}")
624
-
625
  return calculated_subtotal, updated_prompt
626
 
627
  def compute_tip(self, alibaba_key, video_file_obj, subtotal, star_rating, user_review, custom_prompt_text):
@@ -671,7 +445,6 @@ class UIHandler:
671
  analysis, tip_disp, total_bill_disp, prompt_out, vid_out = self.compute_tip(
672
  alibaba_key, video_file_obj, subtotal, star_rating, review, prompt
673
  )
674
- #print(analysis, tip_disp, total_bill_disp, prompt_out, vid_out)
675
  invoice = self.update_invoice_summary(*quantities, tip_disp, total_bill_disp)
676
  return analysis, tip_disp, total_bill_disp, prompt_out, vid_out, invoice
677
 
@@ -722,130 +495,162 @@ class App:
722
  with gr.Blocks(title="Video Tip Calculation Interface", theme=gr.themes.Soft(),
723
  css=self.config.CUSTOM_CSS) as interface:
724
  gr.Markdown("## Video Tip Calculation Interface (Structured)")
 
 
 
725
  quantity_inputs = []
726
  subtotal_display = gr.Number(label="Subtotal ($)", value=0.0, interactive=False, visible=False)
727
- with gr.Row():
728
- with gr.Column(scale=2):
729
- gr.Markdown("### 1. Select Food Items")
730
- with gr.Column(elem_id="food-container"):
731
- for item in self.config.FOOD_ITEMS:
732
- with gr.Column():
733
- gr.Image(
734
- value=item["image"],
735
- label=None,
736
- show_label=False,
737
- width=150,
738
- height=150,
739
- interactive=False
740
- )
741
- gr.Markdown(f"**{item['name']}**")
742
- gr.Markdown(f"Price: ${item['price']:.2f}")
743
- q_input = gr.Number(
744
- label="Qty",
745
- value=0,
746
- minimum=0,
747
- step=1,
748
- elem_id=f"qty_{item['name'].replace(' ', '_')}"
749
- )
750
- quantity_inputs.append(q_input)
751
- subtotal_visible_display = gr.Textbox(label="Subtotal", value="$0.00", interactive=False)
752
- gr.Markdown("### 2. Service Feedback")
753
- review_input = gr.Textbox(label="Review", placeholder="서비스 리뷰를 작성해주세요.", lines=3)
754
- rating_input = gr.Radio(choices=[1, 2, 3, 4, 5], value=3, label="⭐Star Rating (1-5)⭐", type="value")
755
- gr.Markdown("### 3. Calculate Tip")
756
- with gr.Row():
757
- btn_5 = gr.Button("5%")
758
- btn_10 = gr.Button("10%")
759
- btn_15 = gr.Button("15%")
760
- btn_20 = gr.Button("20%")
761
- btn_25 = gr.Button("25%")
762
  with gr.Row():
763
- qwen_btn = gr.Button("Alibaba-Qwen", variant="tertiary", elem_id="qwen-button")
764
- gr.Markdown("### 4. Results")
765
- tip_display = gr.Textbox(label="Calculated Tip", value="$0.00", interactive=False)
766
- total_bill_display = gr.Textbox(label="Total Bill (Subtotal + Tip)", value="$0.00", interactive=False)
767
- payment_btn = gr.Button("결제하기")
768
- payment_result = gr.Textbox(label="Payment Result", value="", interactive=False)
769
- with gr.Column(scale=1):
770
- gr.Markdown("### 5. Upload & Prompt")
771
- alibaba_key_input = gr.Textbox(label="Alibaba API Key", placeholder="Enter your Alibaba API Key", lines=1)
772
- video_input = gr.Video(label="Upload Service Video")
773
- prompt_display = gr.Textbox(
774
- label="Tip Calculation Prompt (Review/Rating/Subtotal 반영)",
775
- lines=20,
776
- max_lines=30,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
777
  interactive=True,
778
- value=self.config.DEFAULT_PROMPT_TEMPLATE.format(
779
- calculated_subtotal=0.0,
780
- star_rating=3,
781
- user_review="(No user review provided)",
782
- google_reviews=GoogleReviewManager.format_google_reviews(self.config.GOOGLE_REVIEWS)
783
- ).replace("{caption_text}", "{{caption_text}}")
784
  )
785
- gr.Markdown("### 6. AI Analysis")
786
- analysis_display = gr.Textbox(label="AI Analysis", lines=10, max_lines=15, interactive=True)
787
- gr.Markdown("### 7. 청구서")
788
- order_summary_display = gr.Textbox(label="청구서", value="주문한 메뉴가 없습니다.", interactive=True)
789
- subtotal_display.change(
790
- fn=lambda x: f"${x:.2f}",
791
- inputs=[subtotal_display],
792
- outputs=[subtotal_visible_display]
793
- )
794
- inputs_for_prompt_update = quantity_inputs + [rating_input, review_input]
795
- outputs_for_prompt_update = [subtotal_display, prompt_display]
796
- for comp in inputs_for_prompt_update:
797
- comp.change(
798
- fn=self.ui_handler.update_subtotal_and_prompt,
799
- inputs=inputs_for_prompt_update,
800
- outputs=outputs_for_prompt_update
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
801
  )
802
- for comp in quantity_inputs:
803
- comp.change(
804
- fn=self.ui_handler.update_invoice_summary,
805
- inputs=quantity_inputs,
806
- outputs=order_summary_display
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
807
  )
808
- compute_inputs = [alibaba_key_input, video_input, subtotal_display, rating_input, review_input, prompt_display] + quantity_inputs
809
- compute_outputs = [
810
- analysis_display, tip_display, total_bill_display, prompt_display, video_input, order_summary_display
811
- ]
812
- qwen_btn.click(
813
- fn=lambda alibaba_key, vid, sub, rat, rev, prom, *qty: self.ui_handler.auto_tip_and_invoice(
814
- alibaba_key, vid, sub, rat, rev, prom, *qty
815
- ),
816
- inputs=compute_inputs,
817
- outputs=compute_outputs
818
- )
819
- btn_5.click(
820
- fn=lambda sub, *qty: self.ui_handler.manual_tip_and_invoice(5, sub, *qty),
821
- inputs=[subtotal_display] + quantity_inputs,
822
- outputs=[analysis_display, tip_display, total_bill_display, order_summary_display]
823
- )
824
- btn_10.click(
825
- fn=lambda sub, *qty: self.ui_handler.manual_tip_and_invoice(10, sub, *qty),
826
- inputs=[subtotal_display] + quantity_inputs,
827
- outputs=[analysis_display, tip_display, total_bill_display, order_summary_display]
828
- )
829
- btn_15.click(
830
- fn=lambda sub, *qty: self.ui_handler.manual_tip_and_invoice(15, sub, *qty),
831
- inputs=[subtotal_display] + quantity_inputs,
832
- outputs=[analysis_display, tip_display, total_bill_display, order_summary_display]
833
- )
834
- btn_20.click(
835
- fn=lambda sub, *qty: self.ui_handler.manual_tip_and_invoice(20, sub, *qty),
836
- inputs=[subtotal_display] + quantity_inputs,
837
- outputs=[analysis_display, tip_display, total_bill_display, order_summary_display]
838
- )
839
- btn_25.click(
840
- fn=lambda sub, *qty: self.ui_handler.manual_tip_and_invoice(25, sub, *qty),
841
- inputs=[subtotal_display] + quantity_inputs,
842
- outputs=[analysis_display, tip_display, total_bill_display, order_summary_display]
843
- )
844
- payment_btn.click(
845
- fn=self.ui_handler.process_payment,
846
- inputs=[total_bill_display],
847
- outputs=[payment_result]
848
- )
849
  return interface
850
 
851
  def run_gradio(self):
 
3
  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:
 
40
  "3. **Overall Service Quality Evaluation**:\n"
41
  " Evaluate the service quality by evenly scoring the following four components, each out of 100:\n"
42
  " a) **Video Service Score**: Service quality observed in the video (from Video Caption).\n"
43
+ " b) **Google Review Score**: Please provide an overall analysis of the recent Google reviews and give the general rating score. Highlight any significant social issues mentioned in the reviews, such as racist comments, discriminatory behavior, or other major societal concerns. The analysis should consider all reviews collectively, not individually. Regardless of the rating score, tips should be calculated very low.\n"
44
+ " - **Note**: If any review mentions racist, sexist, or any other ethical violations, all Google Review scores should automatically be set to 0. This is to account for the severity of such violations. "
45
+ " - Additionally, when such violations are found, the tip should be calculated **very low**, even lower than the typical range for poor service.\n"
46
  " c) **User Review Score**: The user's review (if the review mentions improvements or enhanced service, especially if it indicates that previously reported issues like racism have been resolved, adjust the negative impact accordingly).\n"
47
  " d) **Star Rating Score**: The user's star rating, interpreted on a scale where 5/5 corresponds to 100 points.\n"
48
  " - Calculate the overall average score by taking the mean of these four scores.\n\n"
 
60
  " - Even though all four factors are evaluated equally, if the user's review explicitly indicates improvements (e.g., stating 'There's no racism!' or similar phrases), then increase the Google Review score by adjusting or restoring it to a value higher than the score initially reduced due to negative comments. In other words, if improvements are noted, add a positive increment (using a '+' adjustment) to the Google Review score, ensuring it is higher than the negatively impacted value.\n"
61
  " - Ensure that the user's review score takes priority when there is a conflict with the negative tone of the Google Reviews.\n\n"
62
 
63
+ "### Ethical Violations and Tip Adjustment ###\n"
64
+ " - If there are any ethical violations such as racist, discriminatory, or offensive remarks in the Google reviews, regardless of the review score, the tip should be significantly reduced. "
65
+ " - In these cases, the tip should be calculated at a very low percentage, often close to 0%, to reflect the severity of such behavior. "
66
+ " - This adjustment should result in an even lower tip amount than would be calculated for poor service alone.\n\n"
67
+
68
  "### User Context ###\n"
69
  " - Current Country: USA\n"
70
  " - Restaurant Name: The Golden Spoon (Assumed)\n"
 
73
  " - Currently User Review: {user_review}\n\n"
74
 
75
  "### Recent Google Review ###\n\n"
 
 
 
76
 
77
+ "#### Google Review 1 ####\n"
78
+ "[4.0 stars] The atmosphere, the taste of the steak, and the service were all good. It was unique and fun to be able to choose after tasting the dessert tea and coffee. The regrettable points are: 1. The degree of meat cooking must be uniform in the course meal. Everyone has different tastes, but if the degree of cooking must be uniform because it is a course meal, I think I will consider visiting again. Those who are thinking about a course meal, please take note. 2. When serving all the course meals, they pretended not to notice that they spilled something on the table^^ and when they poured water, they spilled a lot, which was a bit embarrassing. 3. I think they could have explained it more comfortably before serving."
79
+ "\n\n"
80
+ "#### Google Review 2 ####\n"
81
+ "[4.0 stars] A steak restaurant with an American-style atmosphere. Thick tenderloin and strips served? The sound stimulated my appetite, but there was a lot of food, so I left it behind. It was a bit difficult to eat because it was undercooked in the middle. I also had stir-fried kimchi as a garnish, and it was a little sweet, so it tasted like Southeast Asia. The ice cream I had for dessert was delicious, and there was a lot of food left over."
82
+ "\n\n"
83
+
84
  "### Input ###\n"
85
  "Video Caption:\n{{caption_text}}\n\n"
86
 
 
93
  "Overall Analysis: [Step-by-step explanation detailing:\n"
94
  " - How the bill amount was determined;\n"
95
  " - How each of the four components was scored and how any negative scores (e.g., for racism) were adjusted based on improvement indications in the user's review;\n"
96
+ " - If any review mentions racist, sexist, or other ethical violations, the Google Review score is automatically set to 0, and a **very low** tip percentage is calculated. This reflects the severity of such violations and ensures that the tip is significantly reduced.\n"
97
  " - How the overall average score was calculated;\n"
98
  " - The reasoning for the final service quality classification based on the average score;\n"
99
+ " - How the tip percentage was chosen within the guideline range and the detailed calculation for the tip amount and final total bill, taking into account the 0% tip percentage due to the ethical violations.]\n\n"
100
+
101
 
102
+ "### Example Output Indicators (for reference only) ###\n"
103
+ "**Final Tip Percentage**: 12.5%\n"
104
+ "**Final Tip Amount**: $6.25\n"
105
+ "**Final Total Bill**: $56.25\n\n"
106
+
107
  "### Example Output Indicators (for reference only) ###\n"
108
  "**Final Tip Percentage**: 12.5%\n"
109
  "**Final Tip Amount**: $6.25\n"
 
113
  "**Final Tip Percentage**: [X]% (only floating point)\n"
114
  "**Final Tip Amount**: $[Calculated Tip]\n"
115
  "**Final Total Bill**: $[Subtotal + Tip]\n"
116
+
117
+ "\n\nIn Final Answer,The ### Output Indicators ### must strictly follow the format of ### Example Output Indicators (for reference only) ###\n\n"
118
+ "### FINAL ANSWER: **THIS INSTRUCTION ENSURES THAT ONLY THE NECESSARY SPECIAL CHARACTERS THAT ARE PART OF THE REQUIRED OUTPUT FORMAT (E.G., **, $, %) ARE USED, AND ANY OTHER SPECIAL CHARACTERS SUCH AS \\textbf, LATEX COMMANDS, OR ANY NON-STANDARD CHARACTERS SHOULD BE EXCLUDED.**"
119
  )
120
 
121
  CUSTOM_CSS = """
 
137
  """
138
 
139
  def __init__(self):
 
 
 
 
 
 
 
140
  if not os.path.exists("images"):
141
  print("경고: 'images' 폴더를 찾을 수 없습니다. 음식 이미지가 표시되지 않을 수 있습니다.")
142
  for item in self.FOOD_ITEMS:
 
382
  self.video_processor = video_processor
383
 
384
  def update_subtotal_and_prompt(self, *args):
 
385
  num_food_items = len(self.config.FOOD_ITEMS)
386
  quantities = args[:num_food_items]
387
  star_rating = args[num_food_items]
388
  user_review = args[num_food_items + 1]
 
389
  calculated_subtotal = 0.0
390
  for i in range(num_food_items):
391
  calculated_subtotal += self.config.FOOD_ITEMS[i]['price'] * quantities[i]
 
392
  user_review_text = user_review.strip() if user_review and user_review.strip() else "(No user review provided)"
 
 
 
 
393
  updated_prompt = self.config.DEFAULT_PROMPT_TEMPLATE.format(
394
  calculated_subtotal=calculated_subtotal,
395
  star_rating=star_rating,
396
+ user_review=user_review_text
 
397
  )
398
  updated_prompt = updated_prompt.replace("{caption_text}", "{{caption_text}}")
 
399
  return calculated_subtotal, updated_prompt
400
 
401
  def compute_tip(self, alibaba_key, video_file_obj, subtotal, star_rating, user_review, custom_prompt_text):
 
445
  analysis, tip_disp, total_bill_disp, prompt_out, vid_out = self.compute_tip(
446
  alibaba_key, video_file_obj, subtotal, star_rating, review, prompt
447
  )
 
448
  invoice = self.update_invoice_summary(*quantities, tip_disp, total_bill_disp)
449
  return analysis, tip_disp, total_bill_disp, prompt_out, vid_out, invoice
450
 
 
495
  with gr.Blocks(title="Video Tip Calculation Interface", theme=gr.themes.Soft(),
496
  css=self.config.CUSTOM_CSS) as interface:
497
  gr.Markdown("## Video Tip Calculation Interface (Structured)")
498
+
499
+ # --- 컴포넌트 변수 선언 (탭 구조 안에서 정의될 것임) ---
500
+ # 이 변수들은 아래 탭 내부에서 실제 컴포넌트에 할당됩니다.
501
  quantity_inputs = []
502
  subtotal_display = gr.Number(label="Subtotal ($)", value=0.0, interactive=False, visible=False)
503
+ subtotal_visible_display_output = None
504
+ review_input, rating_input = None, None
505
+ btn_5, btn_10, btn_15, btn_20, btn_25 = None, None, None, None, None
506
+ qwen_btn = None
507
+ tip_display, total_bill_display, payment_btn, payment_result = None, None, None, None
508
+ alibaba_key_input, video_input = None, None
509
+ analysis_display, order_summary_display = None, None
510
+ prompt_editor = None # 프롬프트 에디터는 다른 탭에 정의될 것임
511
+
512
+ # --- 최상위 레벨에 탭 구성 ---
513
+ with gr.Tabs():
514
+ # --- 탭 1: 메인 인터페이스 (원래 레이아웃 유지) ---
515
+ with gr.TabItem("Main Interface"):
516
+ # 이 탭 안에 원래의 2단 레이아웃을 그대로 재현
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
517
  with gr.Row():
518
+ # --- 왼쪽 (원래대로) ---
519
+ with gr.Column(scale=2):
520
+ gr.Markdown("### 1. Select Food Items")
521
+ with gr.Column(elem_id="food-container"):
522
+ for item in self.config.FOOD_ITEMS:
523
+ with gr.Column():
524
+ gr.Image(value=item["image"], label=None, show_label=False, width=150,
525
+ height=150, interactive=False)
526
+ gr.Markdown(f"**{item['name']}** (${item['price']:.2f})")
527
+ q_input = gr.Number(label="Qty", value=0, minimum=0, step=1,
528
+ elem_id=f"qty_{item['name'].replace(' ', '_')}")
529
+ quantity_inputs.append(q_input)
530
+
531
+ gr.Markdown("### Subtotal")
532
+ subtotal_visible_display_output = gr.Textbox(value="$0.00", label="Subtotal",
533
+ interactive=False)
534
+
535
+ gr.Markdown("### 2. Service Feedback")
536
+ review_input = gr.Textbox(label="Review", placeholder="서비스 리뷰 작성", lines=3)
537
+ rating_input = gr.Radio(choices=[1, 2, 3, 4, 5], value=3, label="⭐Star Rating (1-5)⭐",
538
+ type="value")
539
+
540
+ gr.Markdown("### 3. Calculate Tip (Manual)")
541
+ with gr.Row():
542
+ btn_5 = gr.Button("5%")
543
+ btn_10 = gr.Button("10%")
544
+ btn_15 = gr.Button("15%")
545
+ btn_20 = gr.Button("20%")
546
+ btn_25 = gr.Button("25%")
547
+ with gr.Row():
548
+ # Qwen 버튼 정의를 여기로 이동!
549
+ qwen_btn = gr.Button("Alibaba-Qwen AI Tip Calculation", variant="primary",
550
+ elem_id="qwen-button")
551
+ gr.Markdown("### 4. Results")
552
+ tip_display = gr.Textbox(label="Calculated Tip", value="$0.00", interactive=False)
553
+ total_bill_display = gr.Textbox(label="Total Bill (Subtotal + Tip)", value="$0.00",
554
+ interactive=False)
555
+ payment_btn = gr.Button("결제하기")
556
+ payment_result = gr.Textbox(label="Payment Result", value="", interactive=False)
557
+
558
+ # --- 오른쪽 열 (원래대로, 프롬프트 디스플레이 제외) ---
559
+ with gr.Column(scale=1):
560
+ gr.Markdown("### 5. Upload & Prompt Access")
561
+ alibaba_key_input = gr.Textbox(label="Alibaba API Key", placeholder="Enter Key", lines=1,
562
+ type="password")
563
+ video_input = gr.Video(label="Upload Service Video")
564
+
565
+ gr.Markdown("### 6. AI Analysis")
566
+ analysis_display = gr.Textbox(label="AI Analysis", lines=8, max_lines=12, interactive=False)
567
+
568
+ gr.Markdown("### 7. 청구서")
569
+ order_summary_display = gr.Textbox(label="청구서", value="주문 메뉴 없음", lines=8, max_lines=12,
570
+ interactive=False)
571
+
572
+ # --- 탭 2: 프롬프트 편집 전용 (넓게!) ---
573
+ with gr.TabItem("Edit Prompt"):
574
+ gr.Markdown("### Prompt Editor")
575
+ gr.Markdown("자동 생성된 프롬프트를 여기서 확인하고 **직접 수정**할 수 있습니다. AI 분석 시 여기에 있는 최종 내용이 사용됩니다.")
576
+ # 이 탭에는 프롬프트 편집기만 배치하여 가로 너비를 최대한 활용
577
+ prompt_editor = gr.Textbox(
578
+ label="Tip Calculation Prompt (Editable)",
579
+ lines=35, # 세로 길이 충분히
580
+ max_lines=60,
581
  interactive=True,
582
+ value="Loading prompt..." # 초기값, 핸들러 통해 업데이트됨
 
 
 
 
 
583
  )
584
+
585
+ # --- 이벤트 핸들러 연결 ---
586
+ # 컴포넌트들이 모두 정의된 후에 연결해야 함
587
+ # (실제 코드에서는 컴포넌트 None 체크 또는 더 나은 구조화 필요)
588
+
589
+ if all([subtotal_display, subtotal_visible_display_output, review_input, rating_input, prompt_editor,
590
+ order_summary_display, alibaba_key_input, video_input, analysis_display, tip_display,
591
+ total_bill_display, payment_result, qwen_btn] + quantity_inputs):
592
+
593
+ # 1. 소계 업데이트 (숨겨진 Number -> 보이는 Textbox)
594
+ subtotal_display.change(
595
+ fn=lambda x: f"${x:.2f}",
596
+ inputs=subtotal_display,
597
+ outputs=subtotal_visible_display_output
598
+ )
599
+
600
+ # 2. 입력 변경 시 -> 소계 계산 및 프롬프트 편집기('Edit Prompt' 탭) 업데이트
601
+ inputs_for_prompt_update = quantity_inputs + [rating_input, review_input]
602
+ outputs_for_prompt_update = [subtotal_display, prompt_editor] # 숨겨진 소계와 다른 탭의 프롬프트 에디터
603
+ for comp in inputs_for_prompt_update:
604
+ comp.change(
605
+ fn=self.ui_handler.update_subtotal_and_prompt,
606
+ inputs=inputs_for_prompt_update,
607
+ outputs=outputs_for_prompt_update
608
+ )
609
+
610
+ # 3. 수량 변경 시 -> 청구서('Main Interface' 탭) 업데이트
611
+ for comp in quantity_inputs:
612
+ comp.change(
613
+ fn=self.ui_handler.update_invoice_summary,
614
+ inputs=quantity_inputs,
615
+ outputs=order_summary_display
616
+ )
617
+
618
+ # 4. AI 계산 버튼('Main Interface' 탭) 클릭 시
619
+ # 입력: 메인 탭의 컴포넌트들 + 'Edit Prompt' 탭의 prompt_editor
620
+ # 출력: 메인 탭의 컴포넌트들 + 'Edit Prompt' 탭의 prompt_editor (업데이트 될 수 있으므로)
621
+ qwen_compute_inputs = [alibaba_key_input, video_input, subtotal_display, rating_input, review_input,
622
+ prompt_editor] + quantity_inputs
623
+ qwen_compute_outputs = [analysis_display, tip_display, total_bill_display, prompt_editor, video_input,
624
+ order_summary_display]
625
+ qwen_btn.click(
626
+ fn=self.ui_handler.auto_tip_and_invoice,
627
+ inputs=qwen_compute_inputs,
628
+ outputs=qwen_compute_outputs
629
  )
630
+
631
+ # 5. 수동 팁 버튼('Main Interface' 탭) 클릭 시
632
+ manual_tip_outputs = [analysis_display, tip_display, total_bill_display, order_summary_display]
633
+ if btn_5: btn_5.click(fn=lambda sub, *qty: self.ui_handler.manual_tip_and_invoice(5, sub, *qty),
634
+ inputs=[subtotal_display] + quantity_inputs, outputs=manual_tip_outputs)
635
+ # ... (btn_10, btn_15, btn_20, btn_25 에 대해서도 동일하게)
636
+ if btn_10: btn_10.click(fn=lambda sub, *qty: self.ui_handler.manual_tip_and_invoice(10, sub, *qty),
637
+ inputs=[subtotal_display] + quantity_inputs, outputs=manual_tip_outputs)
638
+ if btn_15: btn_15.click(fn=lambda sub, *qty: self.ui_handler.manual_tip_and_invoice(15, sub, *qty),
639
+ inputs=[subtotal_display] + quantity_inputs, outputs=manual_tip_outputs)
640
+ if btn_20: btn_20.click(fn=lambda sub, *qty: self.ui_handler.manual_tip_and_invoice(20, sub, *qty),
641
+ inputs=[subtotal_display] + quantity_inputs, outputs=manual_tip_outputs)
642
+ if btn_25: btn_25.click(fn=lambda sub, *qty: self.ui_handler.manual_tip_and_invoice(25, sub, *qty),
643
+ inputs=[subtotal_display] + quantity_inputs, outputs=manual_tip_outputs)
644
+
645
+ # 6. 결제 버튼('Main Interface' 탭) 클릭 시
646
+ if payment_btn: payment_btn.click(
647
+ fn=self.ui_handler.process_payment,
648
+ inputs=[total_bill_display],
649
+ outputs=[payment_result]
650
  )
651
+ else:
652
+ print("Warning: Component initialization might be out of order for event handlers.")
653
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
654
  return interface
655
 
656
  def run_gradio(self):
requirements.txt CHANGED
@@ -1,6 +1,4 @@
1
  gradio
2
  opencv-python
3
  flask
4
- openai
5
- webdriver-manager
6
- selenium
 
1
  gradio
2
  opencv-python
3
  flask
4
+ openai