- .gitignore +1 -0
- app.py +177 -372
- 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**:
|
|
|
|
|
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 |
-
|
728 |
-
|
729 |
-
|
730 |
-
|
731 |
-
|
732 |
-
|
733 |
-
|
734 |
-
|
735 |
-
|
736 |
-
|
737 |
-
|
738 |
-
|
739 |
-
|
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 |
-
|
764 |
-
|
765 |
-
|
766 |
-
|
767 |
-
|
768 |
-
|
769 |
-
|
770 |
-
|
771 |
-
|
772 |
-
|
773 |
-
|
774 |
-
|
775 |
-
|
776 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
777 |
interactive=True,
|
778 |
-
value=
|
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 |
-
|
786 |
-
|
787 |
-
|
788 |
-
|
789 |
-
|
790 |
-
|
791 |
-
|
792 |
-
|
793 |
-
|
794 |
-
|
795 |
-
|
796 |
-
|
797 |
-
|
798 |
-
|
799 |
-
|
800 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
801 |
)
|
802 |
-
|
803 |
-
|
804 |
-
|
805 |
-
|
806 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
807 |
)
|
808 |
-
|
809 |
-
|
810 |
-
|
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
|
|
|
|