kfkas commited on
Commit
862e4a8
Β·
1 Parent(s): b6d5522
.gitattributes CHANGED
@@ -33,6 +33,7 @@ saved_model/**/* 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
  *.mp3 filter=lfs diff=lfs merge=lfs -text
38
  video/* 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
+ *.mp4 filter=lfs diff=lfs merge=lfs -text
37
  *.task filter=lfs diff=lfs merge=lfs -text
38
  *.mp3 filter=lfs diff=lfs merge=lfs -text
39
  video/* filter=lfs diff=lfs merge=lfs -text
app.py ADDED
@@ -0,0 +1,776 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import shutil
3
+ import time
4
+
5
+ import cv2
6
+ import base64
7
+ import uuid
8
+ import re
9
+ from flask import Flask
10
+ import gradio as gr
11
+ from google import genai
12
+
13
+
14
+ # --- Config 클래슀 (Gemma, GPT4o 제거, Qwen만 μ‚¬μš©) ---
15
+ class Config:
16
+ """μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ„€μ • 및 μƒμˆ˜"""
17
+ FOOD_ITEMS = [
18
+ {"name": "짜μž₯λ©΄", "image": "images/food1.jpg", "price": 7.00},
19
+ {"name": "짬뽕", "image": "images/food2.jpg", "price": 8.50},
20
+ {"name": "νƒ•μˆ˜μœ‘", "image": "images/food3.jpg", "price": 15.00},
21
+ {"name": "볢음λ°₯", "image": "images/food4.jpg", "price": 7.50},
22
+ {"name": "깐풍기", "image": "images/food5.jpg", "price": 18.00},
23
+ {"name": "λ§ˆνŒŒλ‘λΆ€", "image": "images/food6.jpg", "price": 12.00},
24
+ {"name": "콜라", "image": "images/food6.jpg", "price": 12.00},
25
+ {"name": "사이닀", "image": "images/food6.jpg", "price": 12.00},
26
+ ]
27
+ # μ•Œλ¦¬λ°”λ°” Qwen API ν‚€ (기본값은 빈 λ¬Έμžμ—΄)
28
+ QWEN_API_KEY = "sk-2424f0bd26d64fe5a7f3a2bd407adc76"
29
+ GEMINI_API_key = "AIzaSyCc8lcm2cZo3ZeMgi4QN1IHSvn9BBpLnz4"
30
+ DEFAULT_PROMPT_TEMPLATE = (
31
+ """
32
+ ### Persona ###
33
+ You are an expert tip calculation assistant focusing on service quality observed in video captions, and you also consider the user's review, star rating, and recent Google reviews.
34
+ Your role is to evaluate all these aspects, assign scores, and calculate the overall average score to determine the appropriate tip percentage, with special handling for ethical violations.
35
+
36
+ ### Task ###
37
+ 1. **Video Analysis**: Analyze the service quality depicted in the provided **Video Caption(s)**. If multiple captions are given, perform a **holistic evaluation** based on the overall impression conveyed by all captions combined. **Focus on significant staff actions, expressions, and interactions described across the captions.** Assign a single, overall **Video Service Score** out of 100 based on this comprehensive analysis, rather than averaging scores from individual captions.
38
+
39
+ 2. **Bill Amount Determination**: Determine the bill amount by following these steps:
40
+ * Use the 'Calculated Subtotal' provided.
41
+
42
+ 3. **Overall Service Quality Evaluation**:
43
+ Evaluate the service quality by evenly scoring the following four components, each out of 100:
44
+ a) **Video Service Score**: Service quality derived from the holistic analysis of Video Caption(s).
45
+ b) **Google Review Score**: Provide an overall analysis of the recent Google reviews and give the general rating score. Highlight any significant social issues mentioned, such as racist comments or discriminatory behavior. The analysis should consider all reviews collectively.
46
+ * **Note**: If any review mentions racist, sexist, or any other ethical violations, the Google Review score must automatically be set to **0**.
47
+ c) **User Review Score**: The user's review.
48
+ d) **Star Rating Score**: The user's star rating, interpreted on a scale where 5/5 corresponds to 100 points.
49
+ * Calculate the overall average score by taking the mean of these four scores.
50
+
51
+ 4. **Service Quality Classification and Tip Guidelines**:
52
+ * Based on the overall average score, classify the service quality as follows:
53
+ * Poor Service: Overall average score < 60 (Tip range: 0% ~ 5% of the bill)
54
+ * Average Service: Overall average score β‰₯ 60 and < 80 (Tip range: 10% ~ 15% of the bill)
55
+ * Good Service: Overall average score β‰₯ 80 (Tip range: 15% ~ 20% of the bill)
56
+ * Select a specific tip percentage within the appropriate range. **Exception**: If ethical violations were noted (Google Review Score is 0), the Final Tip Percentage **must be 0%**.
57
+ * Calculate the tip amount by multiplying the determined bill amount by the chosen tip percentage (round to two decimal places).
58
+ * Calculate the final total bill by adding the tip amount to the subtotal (round to two decimal places).
59
+
60
+ 5. **Review Prioritization and Score Adjustment**:
61
+ * Even though all four factors are evaluated equally, if the user's review **explicitly states that specific issues previously mentioned in Google Reviews (especially ethical violations like racism) have been resolved or are no longer present**, then the Google Review score should be adjusted upwards from its potentially reduced value (e.g., from 0 back towards a score reflecting the *current* situation described by the user). The user's direct, specific, and current assessment takes precedence over older or contradicted Google Review points in such cases. Ensure the User Review Score reflects the user's sentiment.
62
+
63
+ ### Ethical Violations and Tip Adjustment ###
64
+ * If there are any racist, discriminatory, or offensive remarks found in the Google reviews, regardless of the overall review sentiment or scores from other factors, the Google Review Score is automatically set to **0**. Consequently, the **Final Tip Percentage must be set to 0%** to strongly reflect the severity and unacceptability of such behavior.
65
+
66
+ ### Recent Google Review ###
67
+
68
+ #### Google Review 1 ####
69
+ [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.
70
+
71
+ #### Google Review 2 ####
72
+ [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.
73
+
74
+ ### Video Caption Input ###
75
+ # Multiple captions can be provided under this section
76
+ Video Caption(s):
77
+ {{caption_text}}
78
+
79
+ ### Output ###
80
+ Return your answer in the exact format below, providing the text analyses first, followed by the JSON block containing the final calculations:
81
+
82
+ Video Text Analysis: [Provide a summary of the **significant staff actions, expressions, and interactions** observed across all provided **Video Caption(s)**. Explain how these observations contribute to the overall service impression. Include the single, **holistically determined Video Service Score** (out of 100) based on this combined analysis.]
83
+ Recent Google Review Analysis: [Summary of the insights from the Google Reviews, including any negative or racist comments, with the assigned score (out of 100). If ethical violations result in a 0 score, state this clearly.]
84
+ User Review Analysis: [Summary of the user's review including any improvements or enhanced service mentions, with the assigned score (out of 100). Note if this review led to adjustments in the Google Review score based on explicit statements about resolved issues.]
85
+ Star Rating Analysis: [Interpret the user's star rating (e.g., converting 5/5 to 100 points) and include the assigned score (out of 100).]
86
+ Overall Analysis: [Step-by-step explanation detailing:
87
+ - How the bill amount was determined;
88
+ - How each of the four components was scored, including the holistic evaluation of video captions and any adjustments made to the Google Review score based on specific user statements about resolved issues;
89
+ - If any review mentioned racist, sexist, or other ethical violations, explicitly state that the Google Review score was set to 0 and the **Final Tip Percentage is consequently set to 0%**, reflecting the severity;
90
+ - How the overall average score was calculated;
91
+ - The reasoning for the final service quality classification based on the average score;
92
+ - How the final tip percentage was chosen (either based on the guideline range or mandatorily set to 0% due to ethical violations) and the detailed calculation for the tip amount and final total bill.
93
+
94
+ ### Final Calculation Results (JSON Format) ###
95
+ ```json
96
+ {{{{
97
+ "final_tip_percentage": <calculated_percentage_int>,
98
+ "final_tip_amount": <calculated_tip_float>,
99
+ "final_total_bill": <calculated_total_bill_float>
100
+ }}}}
101
+ ```
102
+
103
+ ### GUIDE ###
104
+
105
+ In Final Answer, The ### Final Calculation Results (JSON Format) ### section must strictly follow the JSON structure provided above, containing only the JSON object and its calculated numerical values (use floating-point numbers for all values). **DO NOT include the placeholders like <calculated_percentage_float> in the actual final output; replace them with the real calculated numbers.** Ensure no other special characters like **, $, % are used *within* the JSON structure itself, only standard JSON syntax (keys in double quotes, string values in double quotes, numbers without quotes). The text analysis sections preceding the JSON should still follow their specified format.
106
+ Complete all the text analysis and overall analysis sections first, and generate the ### Final Calculation Results (JSON Format) ### section last.
107
+ **THIS INSTRUCTION ENSURES THAT ONLY THE NECESSARY SPECIAL CHARACTERS THAT ARE PART OF THE REQUIRED OUTPUT FORMAT (E.G., standard JSON syntax) ARE USED, AND ANY OTHER SPECIAL CHARACTERS SUCH AS **, $, %, LATEX COMMANDS, OR ANY NON-STANDARD CHARACTERS SHOULD BE EXCLUDED FROM THE FINAL JSON BLOCK.**
108
+
109
+ ### User Context ###
110
+ * Current Country: USA
111
+ * Currently Restaurant Name: The Golden Spoon (Assumed)
112
+ * Currently Calculated Subtotal: ${calculated_subtotal:.2f}
113
+ * Currently User Star Rating: {star_rating} / 5 (5 is the maximum)
114
+ * Currently User Review: {user_review}
115
+
116
+ """
117
+ )
118
+
119
+ CUSTOM_CSS = """
120
+ #food-container {
121
+ display: grid;
122
+ grid-template-columns: repeat(3, 1fr);
123
+ gap: 10px;
124
+ overflow-y: auto;
125
+ height: 600px;
126
+ }
127
+ #qwen-button {
128
+ background-color: #8A2BE2 !important;
129
+ color: white !important;
130
+ border-color: #8A2BE2 !important;
131
+ }
132
+ #qwen-button:hover {
133
+ background-color: #7722CC !important;
134
+ }
135
+
136
+ #google-button {
137
+ background-color: #50C878 !important;
138
+ color: white !important;
139
+ border-color: #50C878 !important;
140
+ }
141
+ #google-button:hover {
142
+ background-color: #3CB371 !important;
143
+ }
144
+ """
145
+
146
+ def __init__(self):
147
+ if not os.path.exists("images"):
148
+ print("κ²½κ³ : 'images' 폴더λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. μŒμ‹ 이미지가 ν‘œμ‹œλ˜μ§€ μ•Šμ„ 수 μžˆμŠ΅λ‹ˆλ‹€.")
149
+ for item in self.FOOD_ITEMS:
150
+ if not os.path.exists(item["image"]):
151
+ print(f"κ²½κ³ : 이미지 νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€ - {item['image']}")
152
+
153
+
154
+ # --- ModelClients (μ•Œλ¦¬λ°”λ°” Qwen API만 μ‚¬μš©) ---
155
+ class ModelClients:
156
+ def __init__(self, config: Config):
157
+ self.config = config
158
+ from openai import OpenAI as QwenOpenAI
159
+ self.qwen_client = QwenOpenAI(
160
+ api_key=config.QWEN_API_KEY,
161
+ base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
162
+ )
163
+ self.gemini_client = genai.Client(api_key=config.GEMINI_API_key)
164
+
165
+ def encode_video_qwen(self, video_path):
166
+ with open(video_path, "rb") as video_file:
167
+ return base64.b64encode(video_file.read()).decode("utf-8")
168
+
169
+
170
+ # --- VideoProcessor: λΉ„λ””μ˜€ ν”„λ ˆμž„ μΆ”μΆœ ---
171
+ class VideoProcessor:
172
+ def extract_video_frames(self, video_path, output_folder=None, fps=1):
173
+ if not video_path:
174
+ return [], None
175
+ if output_folder is None:
176
+ output_folder = f"frames_list/frames_{uuid.uuid4().hex}"
177
+ os.makedirs(output_folder, exist_ok=True)
178
+ cap = cv2.VideoCapture(video_path)
179
+ if not cap.isOpened():
180
+ print(f"였λ₯˜: λΉ„λ””μ˜€ νŒŒμΌμ„ μ—΄ 수 μ—†μŠ΅λ‹ˆλ‹€ - {video_path}")
181
+ return [], None
182
+ frame_paths = []
183
+ frame_rate = cap.get(cv2.CAP_PROP_FPS)
184
+ if not frame_rate or frame_rate == 0:
185
+ print("κ²½κ³ : FPSλ₯Ό 읽을 수 μ—†μŠ΅λ‹ˆλ‹€, κΈ°λ³Έκ°’ 4으둜 μ„€μ •ν•©λ‹ˆλ‹€.")
186
+ frame_rate = 4.0
187
+ frame_interval = int(frame_rate / fps) if fps > 0 else 1
188
+ if frame_interval <= 0:
189
+ frame_interval = 1
190
+ frame_count = 0
191
+ saved_frame_count = 0
192
+ while cap.isOpened():
193
+ ret, frame = cap.read()
194
+ if not ret:
195
+ break
196
+ if frame is None:
197
+ print(f"κ²½κ³ : {frame_count}번째 ν”„λ ˆμž„μ΄ λΉ„μ–΄μžˆμŠ΅λ‹ˆλ‹€.")
198
+ frame_count += 1
199
+ continue
200
+ if frame_count % frame_interval == 0:
201
+ frame_path = os.path.join(output_folder, f"frame_{saved_frame_count}.jpg")
202
+ try:
203
+ if cv2.imwrite(frame_path, frame):
204
+ frame_paths.append(frame_path)
205
+ saved_frame_count += 1
206
+ else:
207
+ print(f"κ²½κ³ : {frame_path} μ €μž₯ μ‹€νŒ¨.")
208
+ except Exception as e:
209
+ print(f"κ²½κ³ : ν”„λ ˆμž„ μ €μž₯ 였λ₯˜ ({frame_path}): {e}")
210
+ frame_count += 1
211
+ cap.release()
212
+ if not frame_paths:
213
+ print("κ²½κ³ : ν”„λ ˆμž„ μΆ”μΆœ μ‹€νŒ¨.")
214
+ if os.path.exists(output_folder):
215
+ shutil.rmtree(output_folder)
216
+ return [], None
217
+ return frame_paths, output_folder
218
+
219
+ def cleanup_temp_files(self, video_path, frame_folder):
220
+ if video_path and "temp_video_" in video_path and os.path.exists(video_path):
221
+ try:
222
+ os.remove(video_path)
223
+ print(f"μž„μ‹œ λΉ„λ””μ˜€ 파일 μ‚­μ œ: {video_path}")
224
+ except OSError as e:
225
+ print(f"μž„μ‹œ λΉ„λ””μ˜€ 파일 μ‚­μ œ 였λ₯˜: {e}")
226
+ if frame_folder and os.path.exists(frame_folder):
227
+ try:
228
+ shutil.rmtree(frame_folder)
229
+ print(f"ν”„λ ˆμž„ 폴더 μ‚­μ œ: {frame_folder}")
230
+ except OSError as e:
231
+ print(f"ν”„λ ˆμž„ 폴더 μ‚­μ œ 였λ₯˜: {e}")
232
+
233
+
234
+ # --- TipCalculator (μ•Œλ¦¬λ°”λ°” Qwen APIλ₯Ό μ‚¬μš©ν•œ 팁 계산) ---
235
+ class TipCalculator:
236
+ def __init__(self, config: Config, model_clients: ModelClients, video_processor: VideoProcessor):
237
+ self.config = config
238
+ self.model_clients = model_clients
239
+ self.video_processor = video_processor
240
+
241
+ def parse_llm_output(self, output_text):
242
+ """LLM 좜λ ₯을 νŒŒμ‹±ν•˜μ—¬ 팁 계산 κ²°κ³Ό μΆ”μΆœ"""
243
+ analysis = "Analysis not found."
244
+ tip_percentage = 0.0
245
+ tip_amount = 0.0
246
+ total_bill = 0.0
247
+
248
+ # Analysis λΆ€λΆ„: "Analysis:" 이후뢀터 "**Final Tip Percentage**" μ΄μ „κΉŒμ§€ μΆ”μΆœ
249
+ analysis_match = re.search(r"Analysis:\s*(.*?)\*\*Final Tip Percentage\*\*", output_text,
250
+ re.DOTALL | re.IGNORECASE)
251
+ if analysis_match:
252
+ analysis = analysis_match.group(1).strip()
253
+ else:
254
+ analysis_match_alt = re.search(r"Analysis:\s*(.*)", output_text, re.DOTALL | re.IGNORECASE)
255
+ if analysis_match_alt:
256
+ analysis = analysis_match_alt.group(1).strip()
257
+
258
+ # **Final Tip Percentage** μΆ”μΆœ (예: **Final Tip Percentage**: 2.00%)
259
+ percentage_match = re.search(r"\*\*Final Tip Percentage\*\*:\s*([0-9]+(?:\.[0-9]+)?)%", output_text,
260
+ re.DOTALL | re.IGNORECASE)
261
+ if percentage_match:
262
+ try:
263
+ tip_percentage = float(percentage_match.group(1))
264
+ except ValueError:
265
+ print(f"κ²½κ³ : Tip Percentage λ³€ν™˜ μ‹€νŒ¨ - {percentage_match.group(1)}")
266
+ tip_percentage = 0.0
267
+
268
+ # **Final Tip Amount** μΆ”μΆœ (예: **Final Tip Amount**: $1.44)
269
+ tip_match = re.search(r"\*\*Final Tip Amount\*\*:\s*\$?\s*([0-9]+(?:\.[0-9]+)?)", output_text, re.IGNORECASE)
270
+ if tip_match:
271
+ try:
272
+ tip_amount = float(tip_match.group(1))
273
+ except ValueError:
274
+ print(f"κ²½κ³ : Tip Amount λ³€ν™˜ μ‹€νŒ¨ - {tip_match.group(1)}")
275
+ tip_amount = 0.0
276
+ else:
277
+ print(f"κ²½κ³ : 좜λ ₯μ—μ„œ Tip Amountλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€:\n{output_text}")
278
+
279
+ # **Final Total Bill** μΆ”μΆœ (예: **Final Total Bill**: $73.44)
280
+ total_match = re.search(r"\*\*Final Total Bill\*\*:\s*\$?\s*([0-9]+(?:\.[0-9]+)?)", output_text, re.IGNORECASE)
281
+ if total_match:
282
+ try:
283
+ total_bill = float(total_match.group(1))
284
+ except ValueError:
285
+ print(f"κ²½κ³ : Total Bill λ³€ν™˜ μ‹€νŒ¨ - {total_match.group(1)}")
286
+ if len(analysis) < 20 and analysis == "Analysis not found.":
287
+ analysis = output_text
288
+
289
+ return analysis, tip_percentage, tip_amount, output_text
290
+
291
+ def process_tip_qwen(self, video_file_path, star_rating, user_review, calculated_subtotal, custom_prompt=None):
292
+ if not os.path.exists(video_file_path):
293
+ return "Error: λΉ„λ””μ˜€ 파일 κ²½λ‘œκ°€ μœ νš¨ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.", 0.0, 0.0, [], None, ""
294
+ base64_video = self.model_clients.encode_video_qwen(video_file_path)
295
+ omni_caption_prompt = '''
296
+ Task 1: Describe the waiters' actions in these restaurant video frames. Please check for mistakes or negative behaviors.
297
+ Task 2: Provide a short chronological summary of the entire scene.
298
+ '''
299
+ omni_result = self.model_clients.qwen_client.chat.completions.create(
300
+ model="qwen2.5-omni-7b",
301
+ messages=[
302
+ {
303
+ "role": "system",
304
+ "content": [{"type": "text", "text": "You are a helpful assistant."}],
305
+ },
306
+ {
307
+ "role": "user",
308
+ "content": [
309
+ {"type": "video_url", "video_url": {"url": f"data:;base64,{base64_video}"}},
310
+ {"type": "text", "text": omni_caption_prompt},
311
+ ],
312
+ },
313
+ ],
314
+ modalities=["text"],
315
+ stream=True,
316
+ stream_options={"include_usage": True},
317
+ )
318
+ all_omni_chunks = list(omni_result)
319
+ caption_text = ""
320
+ for chunk in all_omni_chunks[:-1]:
321
+ if not chunk.choices:
322
+ continue
323
+ if chunk.choices[0].delta.content:
324
+ caption_text += chunk.choices[0].delta.content
325
+ if not caption_text.strip():
326
+ caption_text = "(No caption from Omni)"
327
+ user_review = user_review.strip() if user_review else "(No user review)"
328
+ if custom_prompt is None:
329
+ prompt = self.config.DEFAULT_PROMPT_TEMPLATE.format(
330
+ calculated_subtotal=calculated_subtotal,
331
+ star_rating=star_rating,
332
+ user_review=user_review
333
+ )
334
+ else:
335
+ try:
336
+ prompt = custom_prompt.format(
337
+ calculated_subtotal=calculated_subtotal,
338
+ star_rating=star_rating,
339
+ user_review=user_review
340
+ )
341
+ except:
342
+ prompt = self.config.DEFAULT_PROMPT_TEMPLATE.format(
343
+ calculated_subtotal=calculated_subtotal,
344
+ star_rating=star_rating,
345
+ user_review=user_review
346
+ )
347
+ final_prompt = prompt.replace("{caption_text}", caption_text)
348
+ qvq_result = self.model_clients.qwen_client.chat.completions.create(
349
+ model="qwen2.5-vl-32b-instruct",
350
+ messages=[
351
+ {"role": "system", "content": [{"type": "text", "text": "You are a helpful assistant."}]},
352
+ {"role": "user", "content": [{"type": "text", "text": final_prompt}]},
353
+ ],
354
+ modalities=["text"],
355
+ stream=True,
356
+ )
357
+ all_qvq_chunks = list(qvq_result)
358
+ final_reasoning = ""
359
+ final_answer = ""
360
+ is_answering = False
361
+ for c in all_qvq_chunks[:-1]:
362
+ if not c.choices:
363
+ continue
364
+ d = c.choices[0].delta
365
+ if hasattr(d, "reasoning_content") and d.reasoning_content:
366
+ final_reasoning += d.reasoning_content
367
+ if d.content:
368
+ if not is_answering:
369
+ print("\n" + "=" * 20 + "Complete Response" + "=" * 20 + "\n")
370
+ is_answering = True
371
+ final_answer += d.content
372
+ final_text = final_reasoning + "\n" + final_answer
373
+ analysis, tip_percentage, tip_amount, output_text = self.parse_llm_output(final_text)
374
+ return analysis, tip_percentage, tip_amount, [], None, output_text
375
+
376
+ def process_tip_gemini(self, video_file_path, star_rating, user_review, calculated_subtotal, custom_prompt=None):
377
+
378
+ if not video_file_path or not os.path.exists(video_file_path):
379
+ return "λΉ„λ””μ˜€ 파일 κ²½λ‘œκ°€ μœ νš¨ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.", [], None
380
+ image_captioning_prompt = '''
381
+ Task 1: Describe the actions of any waiters or staff visible in these restaurant video. Note any specific interactions, mistakes, or positive actions.
382
+ Task 2: Provide a concise overall summary of the scene depicted in the frames, in chronological order if possible.
383
+
384
+ Task 1 Output:
385
+ Task 2 Output:
386
+ '''
387
+
388
+ video_file = self.model_clients.gemini_client.files.upload(file=video_file_path)
389
+ print(f"Uploaded file info: {video_file}")
390
+
391
+ # λ™μ˜μƒμ€ 처리 쀑(Processing) μƒνƒœμ΄λ―€λ‘œ, ACTIVE μƒνƒœκ°€ 될 λ•ŒκΉŒμ§€ λŒ€κΈ°ν•©λ‹ˆλ‹€.
392
+ while video_file.state.name == "PROCESSING":
393
+ print("λ™μ˜μƒ 처리 쀑...")
394
+ time.sleep(1) # 예: 5μ΄ˆλ§ˆλ‹€ μƒνƒœ 점검
395
+ video_file = self.model_clients.gemini_client.files.get(name=video_file.name)
396
+
397
+ # 처리 μ‹€νŒ¨ μ—¬λΆ€ 확인
398
+ if video_file.state.name == "FAILED":
399
+ raise ValueError(f"파일 처리 μ‹€νŒ¨: {video_file.state.name}")
400
+
401
+ # λΉ„λ””μ˜€λ₯Ό ν”„λ‘¬ν”„νŠΈμ— ν¬ν•¨ν•΄μ„œ μΆ”λ‘  μš”μ²­
402
+ try:
403
+ caption_summary = self.model_clients.gemini_client.models.generate_content(
404
+ model="gemini-2.0-flash-lite", # μ˜ˆμ‹œ: Gemini 2.0 Flash λͺ¨λΈ μ‚¬μš©
405
+ contents=[video_file, image_captioning_prompt]
406
+ )
407
+ caption_summary = caption_summary.text
408
+
409
+ except Exception as e:
410
+ print(f"둜컬 λͺ¨λΈ ν”„λ ˆμž„ 처리 였λ₯˜: {e}")
411
+ return f"Error in processing frames with local model: {e}", None, None
412
+
413
+ user_review = user_review.strip() if user_review and user_review.strip() else "(No user review provided)"
414
+
415
+ if custom_prompt is None:
416
+ prompt = self.config.DEFAULT_PROMPT_TEMPLATE.format(
417
+ calculated_subtotal=calculated_subtotal, star_rating=star_rating, user_review=user_review
418
+ )
419
+ else:
420
+ try:
421
+ prompt = custom_prompt.format(
422
+ calculated_subtotal=calculated_subtotal, star_rating=star_rating, user_review=user_review
423
+ )
424
+ except KeyError as e:
425
+ print(f"κ²½κ³ : μ»€μŠ€ν…€ ν”„λ‘¬ν”„νŠΈμ— ν•„μš”ν•œ ν‚€κ°€ μ—†μŠ΅λ‹ˆλ‹€: {e}. κΈ°λ³Έ ν…œν”Œλ¦Ώμ„ μ‚¬μš©ν•©λ‹ˆλ‹€.")
426
+ prompt = self.config.DEFAULT_PROMPT_TEMPLATE.format(
427
+ calculated_subtotal=calculated_subtotal, star_rating=star_rating, user_review=user_review
428
+ )
429
+
430
+ final_prompt = prompt.replace("{caption_text}", caption_summary)
431
+ messages = final_prompt
432
+
433
+ video_file = self.model_clients.gemini_client.files.upload(file=video_file_path)
434
+ print(f"Uploaded file info: {video_file}")
435
+
436
+ # λ™μ˜μƒμ€ 처리 쀑(Processing) μƒνƒœμ΄λ―€λ‘œ, ACTIVE μƒνƒœκ°€ 될 λ•ŒκΉŒμ§€ λŒ€κΈ°ν•©λ‹ˆλ‹€.
437
+ while video_file.state.name == "PROCESSING":
438
+ print("λ™μ˜μƒ 처리 쀑...")
439
+ time.sleep(1) # 예: 5μ΄ˆλ§ˆλ‹€ μƒνƒœ 점검
440
+ video_file = self.model_clients.gemini_client.files.get(name=video_file.name)
441
+
442
+ # 처리 μ‹€νŒ¨ μ—¬λΆ€ 확인
443
+ if video_file.state.name == "FAILED":
444
+ raise ValueError(f"파일 처리 μ‹€νŒ¨: {video_file.state.name}")
445
+
446
+
447
+ response = self.model_clients.gemini_client.models.generate_content(
448
+ model="gemini-2.0-flash-lite", # μ˜ˆμ‹œ: Gemini 2.0 Flash λͺ¨λΈ μ‚¬μš©
449
+ contents=[video_file, messages]
450
+ )
451
+ llm_output = response.text
452
+ analysis, tip_percentage, tip_amount, output_text = self.parse_llm_output(llm_output)
453
+ return analysis, tip_percentage, tip_amount, [], None, output_text
454
+
455
+
456
+ def calculate_manual_tip(self, tip_percent, subtotal):
457
+ tip_amount = subtotal * (tip_percent / 100)
458
+ total_bill = subtotal + tip_amount
459
+ analysis_output = f"Manual calculation using fixed tip percentage of {tip_percent}%."
460
+ tip_output = f"${tip_amount:.2f} ({tip_percent:.1f}%)"
461
+ total_bill_output = f"${total_bill:.2f}"
462
+ return analysis_output, tip_output, total_bill_output
463
+
464
+
465
+ # --- UIHandler: Gradio μΈν„°νŽ˜μ΄μŠ€ 이벀트 처리 (μ•Œλ¦¬λ°”λ°” API ν‚€ μž…λ ₯ 포함) ---
466
+ class UIHandler:
467
+ def __init__(self, config: Config, tip_calculator: TipCalculator, video_processor: VideoProcessor):
468
+ self.config = config
469
+ self.tip_calculator = tip_calculator
470
+ self.video_processor = video_processor
471
+
472
+ def update_subtotal_and_prompt(self, *args):
473
+ num_food_items = len(self.config.FOOD_ITEMS)
474
+ quantities = args[:num_food_items]
475
+ star_rating = args[num_food_items]
476
+ user_review = args[num_food_items + 1]
477
+ calculated_subtotal = 0.0
478
+ for i in range(num_food_items):
479
+ calculated_subtotal += self.config.FOOD_ITEMS[i]['price'] * quantities[i]
480
+ user_review_text = user_review.strip() if user_review and user_review.strip() else "(No user review provided)"
481
+ updated_prompt = self.config.DEFAULT_PROMPT_TEMPLATE.format(
482
+ calculated_subtotal=calculated_subtotal,
483
+ star_rating=star_rating,
484
+ user_review=user_review_text
485
+ )
486
+ updated_prompt = updated_prompt.replace("{caption_text}", "{{caption_text}}")
487
+ return calculated_subtotal, updated_prompt
488
+
489
+ def compute_tip(self, type, video_file_obj, subtotal, star_rating, user_review, custom_prompt_text):
490
+ analysis_output = "계산을 μ‹œμž‘ν•©λ‹ˆλ‹€..."
491
+ tip_percentage = 0.0
492
+ tip_output = "$0.00"
493
+ total_bill_output = f"${subtotal:.2f}"
494
+ if video_file_obj is None:
495
+ return "였λ₯˜: λΉ„λ””μ˜€ νŒŒμΌμ„ μ—…λ‘œλ“œν•΄μ£Όμ„Έμš”.", "$0.00", total_bill_output, custom_prompt_text, gr.update(value=None)
496
+ try:
497
+ temp_video_path = f"temp_video_{uuid.uuid4().hex}.mp4"
498
+ original_path = video_file_obj.name if hasattr(video_file_obj, 'name') else video_file_obj
499
+ shutil.copyfile(original_path, temp_video_path)
500
+ print(f"μž„μ‹œ λΉ„λ””μ˜€ 파일 생성: {temp_video_path}")
501
+ except Exception as e:
502
+ print(f"μž„μ‹œ λΉ„λ””μ˜€ 파일 생성 였λ₯˜: {e}")
503
+ return f"였λ₯˜: λΉ„λ””μ˜€ νŒŒμΌμ„ μ²˜λ¦¬ν•  수 μ—†μŠ΅λ‹ˆλ‹€: {e}", "$0.00", total_bill_output, custom_prompt_text, None
504
+ frame_folder = None
505
+ try:
506
+ if type == 'qwen':
507
+ analysis, tip_percentage, tip_amount, _, _, output_text = self.tip_calculator.process_tip_qwen(
508
+ temp_video_path, star_rating, user_review, subtotal, custom_prompt_text
509
+ )
510
+ else:
511
+ analysis, tip_percentage, tip_amount, _, _, output_text = self.tip_calculator.process_tip_gemini(
512
+ temp_video_path, star_rating, user_review, subtotal, custom_prompt_text
513
+ )
514
+ if "Error" in analysis:
515
+ analysis_output = analysis
516
+ tip_amount = 0.0
517
+ else:
518
+ analysis_output = f"Tip Percentage: {tip_percentage:.1f}%\n\n{output_text}"
519
+ tip_output = f"${tip_amount:.2f} ({tip_percentage:.1f}%)"
520
+ total_bill = subtotal + tip_amount
521
+ total_bill_output = f"${total_bill:.2f}"
522
+ except Exception as e:
523
+ print(f"팁 계산 쀑 였λ₯˜ λ°œμƒ (qwen): {e}")
524
+ analysis_output = f"였λ₯˜ λ°œμƒ: {e}"
525
+ tip_output = "$0.00"
526
+ total_bill_output = f"${subtotal:.2f}"
527
+ finally:
528
+ self.video_processor.cleanup_temp_files(temp_video_path, frame_folder)
529
+ return analysis_output, tip_output, total_bill_output, custom_prompt_text, gr.update(value=None)
530
+
531
+ def auto_tip_and_invoice(self, type, video_file_obj, subtotal, star_rating, review, prompt, *quantities):
532
+ analysis, tip_disp, total_bill_disp, prompt_out, vid_out = self.compute_tip(
533
+ type, video_file_obj, subtotal, star_rating, review, prompt
534
+ )
535
+ invoice = self.update_invoice_summary(*quantities, tip_disp, total_bill_disp)
536
+ return analysis, tip_disp, total_bill_disp, prompt_out, vid_out, invoice
537
+
538
+ def update_invoice_summary(self, *args):
539
+ num_items = len(self.config.FOOD_ITEMS)
540
+ quantities = args[:num_items]
541
+ if len(args) >= num_items + 2:
542
+ tip_str = args[num_items]
543
+ total_bill_str = args[num_items + 1]
544
+ else:
545
+ tip_str = "$0.00"
546
+ total_bill_str = "$0.00"
547
+ summary = ""
548
+ for i, q in enumerate(quantities):
549
+ try:
550
+ q_val = float(q)
551
+ except:
552
+ q_val = 0
553
+ if q_val > 0:
554
+ item = self.config.FOOD_ITEMS[i]
555
+ total_price = item['price'] * q_val
556
+ summary += f"{item['name']} x{int(q_val)} : ${total_price:.2f}\n"
557
+ if summary == "":
558
+ summary = "μ£Όλ¬Έν•œ 메뉴가 μ—†μŠ΅λ‹ˆλ‹€."
559
+ summary += f"\nTip: {tip_str}\nTotal Bill: {total_bill_str}"
560
+ return summary
561
+
562
+ def manual_tip_and_invoice(self, tip_percent, subtotal, *quantities):
563
+ analysis, tip_disp, total_bill_disp = self.tip_calculator.calculate_manual_tip(tip_percent, subtotal)
564
+ invoice = self.update_invoice_summary(*quantities, tip_disp, total_bill_disp)
565
+ return analysis, tip_disp, total_bill_disp, invoice
566
+
567
+ def process_payment(self, total_bill):
568
+ return f"{total_bill} κ²°μ œλ˜μ—ˆμŠ΅λ‹ˆλ‹€."
569
+
570
+
571
+ # --- App: λͺ¨λ“  μ»΄ν¬λ„ŒνŠΈ μ—°κ²° 및 Gradio μΈν„°νŽ˜μ΄μŠ€ μ‹€ν–‰ ---
572
+ class App:
573
+ def __init__(self):
574
+ self.config = Config()
575
+ self.model_clients = ModelClients(self.config)
576
+ self.video_processor = VideoProcessor()
577
+ self.tip_calculator = TipCalculator(self.config, self.model_clients, self.video_processor)
578
+ self.ui_handler = UIHandler(self.config, self.tip_calculator, self.video_processor)
579
+ self.flask_app = Flask(__name__)
580
+
581
+ def create_gradio_blocks(self):
582
+ with gr.Blocks(title="Video Tip Calculation Interface", theme=gr.themes.Soft(),
583
+ css=self.config.CUSTOM_CSS) as interface:
584
+ gr.Markdown("## Video Tip Calculation Interface (Structured)")
585
+
586
+ # --- μ»΄ν¬λ„ŒνŠΈ λ³€μˆ˜ μ„ μ–Έ (νƒ­ ꡬ쑰 μ•ˆμ—μ„œ μ •μ˜λ  κ²ƒμž„) ---
587
+ # 이 λ³€μˆ˜λ“€μ€ μ•„λž˜ νƒ­ λ‚΄λΆ€μ—μ„œ μ‹€μ œ μ»΄ν¬λ„ŒνŠΈμ— ν• λ‹Ήλ©λ‹ˆλ‹€.
588
+ quantity_inputs = []
589
+ subtotal_display = gr.Number(label="Subtotal ($)", value=0.0, interactive=False, visible=False)
590
+ subtotal_visible_display_output = None
591
+ review_input, rating_input = None, None
592
+ btn_5, btn_10, btn_15, btn_20, btn_25 = None, None, None, None, None
593
+ qwen_btn = None
594
+ gemini_btn = None
595
+ tip_display, total_bill_display, payment_btn, payment_result = None, None, None, None
596
+ video_input = None
597
+ analysis_display, order_summary_display = None, None
598
+ prompt_editor = None # ν”„λ‘¬ν”„νŠΈ μ—λ””ν„°λŠ” λ‹€λ₯Έ 탭에 μ •μ˜λ  κ²ƒμž„
599
+
600
+ # --- μ΅œμƒμœ„ λ ˆλ²¨μ— νƒ­ ꡬ성 ---
601
+ with gr.Tabs():
602
+ # --- νƒ­ 1: 메인 μΈν„°νŽ˜μ΄μŠ€ (μ›λž˜ λ ˆμ΄μ•„μ›ƒ μœ μ§€) ---
603
+ with gr.TabItem("Main Interface"):
604
+ # 이 νƒ­ μ•ˆμ— μ›λž˜μ˜ 2단 λ ˆμ΄μ•„μ›ƒμ„ κ·ΈλŒ€λ‘œ μž¬ν˜„
605
+ with gr.Row():
606
+ # --- μ™Όμͺ½ μ—΄ (μ›λž˜λŒ€λ‘œ) ---
607
+ with gr.Column(scale=2):
608
+ gr.Markdown("### 1. Select Food Items")
609
+ with gr.Column(elem_id="food-container"):
610
+ for item in self.config.FOOD_ITEMS:
611
+ with gr.Column():
612
+ gr.Image(value=item["image"], label=None, show_label=False, width=150,
613
+ height=150, interactive=False)
614
+ gr.Markdown(f"**{item['name']}** (${item['price']:.2f})")
615
+ q_input = gr.Number(label="Qty", value=0, minimum=0, step=1,
616
+ elem_id=f"qty_{item['name'].replace(' ', '_')}")
617
+ quantity_inputs.append(q_input)
618
+
619
+ gr.Markdown("### Subtotal")
620
+ subtotal_visible_display_output = gr.Textbox(value="$0.00", label="Subtotal",
621
+ interactive=False)
622
+
623
+ gr.Markdown("### 2. Service Feedback")
624
+ review_input = gr.Textbox(label="Review", placeholder="μ„œλΉ„μŠ€ 리뷰 μž‘μ„±", lines=3)
625
+ rating_input = gr.Radio(choices=[1, 2, 3, 4, 5], value=3, label="⭐Star Rating (1-5)⭐",
626
+ type="value")
627
+
628
+ gr.Markdown("### 3. Calculate Tip (Manual)")
629
+ with gr.Row():
630
+ btn_5 = gr.Button("5%")
631
+ btn_10 = gr.Button("10%")
632
+ btn_15 = gr.Button("15%")
633
+ btn_20 = gr.Button("20%")
634
+ btn_25 = gr.Button("25%")
635
+ with gr.Row():
636
+ # Qwen λ²„νŠΌ μ •μ˜λ₯Ό μ—¬κΈ°λ‘œ 이동!
637
+ gemini_btn = gr.Button("Google-Gemini AI Tip Calculation", variant="secondary", elem_id="google-button")
638
+ qwen_btn = gr.Button("Alibaba-Qwen AI Tip Calculation", variant="primary",
639
+ elem_id="qwen-button")
640
+ gr.Markdown("### 4. Results")
641
+ tip_display = gr.Textbox(label="Calculated Tip", value="$0.00", interactive=False)
642
+ total_bill_display = gr.Textbox(label="Total Bill (Subtotal + Tip)", value="$0.00",
643
+ interactive=False)
644
+ payment_btn = gr.Button("κ²°μ œν•˜κΈ°")
645
+ payment_result = gr.Textbox(label="Payment Result", value="", interactive=False)
646
+
647
+ # --- 였λ₯Έμͺ½ μ—΄ (μ›λž˜λŒ€λ‘œ, ν”„λ‘¬ν”„νŠΈ λ””μŠ€ν”Œλ ˆμ΄ μ œμ™Έ) ---
648
+ with gr.Column(scale=1):
649
+ gr.Markdown("### 5. Upload & Prompt Access")
650
+
651
+ video_input = gr.Video(label="Upload Service Video")
652
+
653
+ gr.Markdown("### 6. AI Analysis")
654
+ analysis_display = gr.Textbox(label="AI Analysis", lines=8, max_lines=12, interactive=False)
655
+
656
+ gr.Markdown("### 7. μ²­κ΅¬μ„œ")
657
+ order_summary_display = gr.Textbox(label="μ²­κ΅¬μ„œ", value="μ£Όλ¬Έ 메뉴 μ—†μŒ", lines=8, max_lines=12,
658
+ interactive=False)
659
+
660
+
661
+ # --- νƒ­ 2: ν”„λ‘¬ν”„νŠΈ νŽΈμ§‘ μ „μš© (λ„“κ²Œ!) ---
662
+ with gr.TabItem("Edit Prompt"):
663
+ gr.Markdown("### Prompt Editor")
664
+ gr.Markdown("μžλ™ μƒμ„±λœ ν”„λ‘¬ν”„νŠΈλ₯Ό μ—¬κΈ°μ„œ ν™•μΈν•˜κ³  **직접 μˆ˜μ •**ν•  수 μžˆμŠ΅λ‹ˆλ‹€. AI 뢄석 μ‹œ 여기에 μžˆλŠ” μ΅œμ’… λ‚΄μš©μ΄ μ‚¬μš©λ©λ‹ˆλ‹€.")
665
+ # 이 νƒ­μ—λŠ” ν”„λ‘¬ν”„νŠΈ νŽΈμ§‘κΈ°λ§Œ λ°°μΉ˜ν•˜μ—¬ κ°€λ‘œ λ„ˆλΉ„λ₯Ό μ΅œλŒ€ν•œ ν™œμš©
666
+ prompt_editor = gr.Code(
667
+ label="Tip Calculation Prompt (Editable)",
668
+ language="python", # ν•˜μ΄λΌμ΄νŒ…μ΄ ν•„μš” μ—†μœΌλ©΄ "text"
669
+ value="Loading prompt...",
670
+ lines=35, # μ„Έλ‘œ 높이.
671
+ )
672
+
673
+ gr.Examples(
674
+ examples=[
675
+ # μž…λ ₯ μˆœμ„œ: [Alibaba API Key, Video, Subtotal, Star Rating, Review] + [각 μŒμ‹μ˜ Qty]
676
+ ["video/sample.mp4", 0.0, 1, "He drop the tray..so bad", 0, 0, 0, 0, 0, 2, 0, 0],
677
+ ["video/sample2.mp4", 0.0, 5, "Good service!", 0, 0, 0, 0, 0, 2, 0, 0]
678
+ ],
679
+ inputs=[video_input, subtotal_display, rating_input,
680
+ review_input] + quantity_inputs,
681
+ outputs=[analysis_display, tip_display, total_bill_display, video_input, order_summary_display],
682
+ label="Example: Bad Service, Good Service"
683
+ )
684
+
685
+ # --- 이벀트 ν•Έλ“€λŸ¬ μ—°κ²° ---
686
+ # μ»΄ν¬λ„ŒνŠΈλ“€μ΄ λͺ¨λ‘ μ •μ˜λœ 후에 μ—°κ²°ν•΄μ•Ό 함
687
+ # (μ‹€μ œ μ½”λ“œμ—μ„œλŠ” μ»΄ν¬λ„ŒνŠΈ None 체크 λ˜λŠ” 더 λ‚˜μ€ ꡬ쑰화 ν•„μš”)
688
+
689
+ if all([subtotal_display, subtotal_visible_display_output, review_input, rating_input, prompt_editor,
690
+ order_summary_display, video_input, analysis_display, tip_display,
691
+ total_bill_display, payment_result, qwen_btn, gemini_btn] + quantity_inputs):
692
+
693
+ # 1. μ†Œκ³„ μ—…λ°μ΄νŠΈ (μˆ¨κ²¨μ§„ Number -> λ³΄μ΄λŠ” Textbox)
694
+ subtotal_display.change(
695
+ fn=lambda x: f"${x:.2f}",
696
+ inputs=subtotal_display,
697
+ outputs=subtotal_visible_display_output
698
+ )
699
+
700
+ # 2. μž…λ ₯ λ³€κ²½ μ‹œ -> μ†Œκ³„ 계산 및 ν”„λ‘¬ν”„νŠΈ νŽΈμ§‘κΈ°('Edit Prompt' νƒ­) μ—…λ°μ΄νŠΈ
701
+ inputs_for_prompt_update = quantity_inputs + [rating_input, review_input]
702
+ outputs_for_prompt_update = [subtotal_display, prompt_editor] # μˆ¨κ²¨μ§„ μ†Œκ³„μ™€ λ‹€λ₯Έ νƒ­μ˜ ν”„λ‘¬ν”„νŠΈ 에디터
703
+ for comp in inputs_for_prompt_update:
704
+ comp.change(
705
+ fn=self.ui_handler.update_subtotal_and_prompt,
706
+ inputs=inputs_for_prompt_update,
707
+ outputs=outputs_for_prompt_update
708
+ )
709
+
710
+ # 3. μˆ˜λŸ‰ λ³€κ²½ μ‹œ -> μ²­κ΅¬μ„œ('Main Interface' νƒ­) μ—…λ°μ΄νŠΈ
711
+ for comp in quantity_inputs:
712
+ comp.change(
713
+ fn=self.ui_handler.update_invoice_summary,
714
+ inputs=quantity_inputs,
715
+ outputs=order_summary_display
716
+ )
717
+
718
+ # 4. AI 계산 λ²„νŠΌ('Main Interface' νƒ­) 클릭 μ‹œ
719
+ # μž…λ ₯: 메인 νƒ­μ˜ μ»΄ν¬λ„ŒνŠΈλ“€ + 'Edit Prompt' νƒ­μ˜ prompt_editor
720
+ # 좜λ ₯: 메인 νƒ­μ˜ μ»΄ν¬λ„ŒνŠΈλ“€ + 'Edit Prompt' νƒ­μ˜ prompt_editor (μ—…λ°μ΄νŠΈ 될 수 μžˆμœΌλ―€λ‘œ)
721
+ qwen_btn.click(
722
+ fn=lambda vid, sub, rat, rev, prom, *qty: self.ui_handler.auto_tip_and_invoice('qwen', vid, sub,
723
+ rat, rev, prom,
724
+ *qty),
725
+ inputs=[video_input, subtotal_display, rating_input, review_input, prompt_editor] + quantity_inputs,
726
+ outputs=[analysis_display, tip_display, total_bill_display, prompt_editor, video_input,
727
+ order_summary_display]
728
+ )
729
+ gemini_btn.click(
730
+ fn=lambda vid, sub, rat, rev, prom, *qty: self.ui_handler.auto_tip_and_invoice('gemini', vid, sub,
731
+ rat, rev, prom,
732
+ *qty),
733
+ inputs=[video_input, subtotal_display, rating_input, review_input, prompt_editor] + quantity_inputs,
734
+ outputs=[analysis_display, tip_display, total_bill_display, prompt_editor, video_input,
735
+ order_summary_display]
736
+ )
737
+
738
+ # 5. μˆ˜λ™ 팁 λ²„νŠΌ('Main Interface' νƒ­) 클릭 μ‹œ
739
+ manual_tip_outputs = [analysis_display, tip_display, total_bill_display, order_summary_display]
740
+ if btn_5: btn_5.click(fn=lambda sub, *qty: self.ui_handler.manual_tip_and_invoice(5, sub, *qty),
741
+ inputs=[subtotal_display] + quantity_inputs, outputs=manual_tip_outputs)
742
+ # ... (btn_10, btn_15, btn_20, btn_25 에 λŒ€ν•΄μ„œλ„ λ™μΌν•˜κ²Œ)
743
+ if btn_10: btn_10.click(fn=lambda sub, *qty: self.ui_handler.manual_tip_and_invoice(10, sub, *qty),
744
+ inputs=[subtotal_display] + quantity_inputs, outputs=manual_tip_outputs)
745
+ if btn_15: btn_15.click(fn=lambda sub, *qty: self.ui_handler.manual_tip_and_invoice(15, sub, *qty),
746
+ inputs=[subtotal_display] + quantity_inputs, outputs=manual_tip_outputs)
747
+ if btn_20: btn_20.click(fn=lambda sub, *qty: self.ui_handler.manual_tip_and_invoice(20, sub, *qty),
748
+ inputs=[subtotal_display] + quantity_inputs, outputs=manual_tip_outputs)
749
+ if btn_25: btn_25.click(fn=lambda sub, *qty: self.ui_handler.manual_tip_and_invoice(25, sub, *qty),
750
+ inputs=[subtotal_display] + quantity_inputs, outputs=manual_tip_outputs)
751
+
752
+ # 6. 결제 λ²„νŠΌ('Main Interface' νƒ­) 클릭 μ‹œ
753
+ if payment_btn: payment_btn.click(
754
+ fn=self.ui_handler.process_payment,
755
+ inputs=[total_bill_display],
756
+ outputs=[payment_result]
757
+ )
758
+ else:
759
+ print("Warning: Component initialization might be out of order for event handlers.")
760
+
761
+ return interface
762
+
763
+ def run_gradio(self):
764
+ interface = self.create_gradio_blocks()
765
+ interface.launch(share=False)
766
+
767
+ def run_flask(self):
768
+ @self.flask_app.route("/")
769
+ def index():
770
+ return "Hello Flask"
771
+ self.flask_app.run(host="0.0.0.0", port=5000, debug=True)
772
+
773
+
774
+ if __name__ == "__main__":
775
+ app = App()
776
+ app.run_gradio()
images/food1.jpg ADDED
images/food2.jpg ADDED
images/food3.jpg ADDED
images/food4.jpg ADDED
images/food5.jpg ADDED
images/food6.jpg ADDED
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ gradio
2
+ opencv-python
3
+ flask
4
+ openai
5
+ google-genai
6
+ google-generativeai
video/sample.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7124bb294dd374389d8da2b25661c6152b7a90eecf97fc4731ba46a292d90680
3
+ size 801305
video/sample2.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1c76e3ac3f9e1db2529f5b429bc2f9a81209f2a1ac8fcae4f7fc765f26c61a4e
3
+ size 2286548