- app.py +11 -150
- requirements.txt +4 -0
- space.yaml +4 -0
app.py
CHANGED
@@ -3,18 +3,13 @@ import shutil
|
|
3 |
import cv2
|
4 |
import base64
|
5 |
import uuid
|
|
|
6 |
from flask import Flask
|
7 |
-
|
8 |
import gradio as gr
|
9 |
-
import re
|
10 |
|
11 |
-
#
|
12 |
-
# Config: 앱 설정 (Gemma와 GPT4o 관련 설정은 제거)
|
13 |
-
# -----------------------------------------------------------------------------
|
14 |
class Config:
|
15 |
"""애플리케이션 설정 및 상수"""
|
16 |
-
|
17 |
-
# 음식 메뉴 데이터
|
18 |
FOOD_ITEMS = [
|
19 |
{"name": "짜장면", "image": "images/food1.jpg", "price": 7.00},
|
20 |
{"name": "짬뽕", "image": "images/food2.jpg", "price": 8.50},
|
@@ -25,11 +20,9 @@ class Config:
|
|
25 |
{"name": "콜라", "image": "images/food6.jpg", "price": 12.00},
|
26 |
{"name": "사이다", "image": "images/food6.jpg", "price": 12.00},
|
27 |
]
|
28 |
-
|
29 |
# 알리바바 Qwen API 키 (기본값은 빈 문자열)
|
30 |
QWEN_API_KEY = ""
|
31 |
|
32 |
-
# 기본 프롬프트 템플릿
|
33 |
DEFAULT_PROMPT_TEMPLATE = (
|
34 |
"### Persona ###\n"
|
35 |
"You are an expert tip calculation assistant focusing on service quality observed in a video.\n\n"
|
@@ -65,7 +58,6 @@ class Config:
|
|
65 |
"Total Bill: $[Subtotal + Tip]"
|
66 |
)
|
67 |
|
68 |
-
# Gradio UI용 CSS
|
69 |
CUSTOM_CSS = """
|
70 |
#food-container {
|
71 |
display: grid;
|
@@ -74,21 +66,17 @@ class Config:
|
|
74 |
overflow-y: auto;
|
75 |
height: 600px;
|
76 |
}
|
77 |
-
|
78 |
-
/* Qwen 버튼을 보라색으로 */
|
79 |
#qwen-button {
|
80 |
background-color: #8A2BE2 !important;
|
81 |
color: white !important;
|
82 |
border-color: #8A2BE2 !important;
|
83 |
}
|
84 |
-
|
85 |
#qwen-button:hover {
|
86 |
background-color: #7722CC !important;
|
87 |
}
|
88 |
"""
|
89 |
|
90 |
def __init__(self):
|
91 |
-
# 이미지 디렉토리 확인
|
92 |
if not os.path.exists("images"):
|
93 |
print("경고: 'images' 폴더를 찾을 수 없습니다. 음식 이미지가 표시되지 않을 수 있습니다.")
|
94 |
for item in self.FOOD_ITEMS:
|
@@ -96,12 +84,8 @@ class Config:
|
|
96 |
print(f"경고: 이미지 파일을 찾을 수 없습니다 - {item['image']}")
|
97 |
|
98 |
|
99 |
-
#
|
100 |
-
# ModelClients: 알리바바 Qwen API 클라이언트만 사용
|
101 |
-
# -----------------------------------------------------------------------------
|
102 |
class ModelClients:
|
103 |
-
"""알리바바 Qwen API 클라이언트 관리"""
|
104 |
-
|
105 |
def __init__(self, config: Config):
|
106 |
self.config = config
|
107 |
from openai import OpenAI as QwenOpenAI
|
@@ -111,56 +95,40 @@ class ModelClients:
|
|
111 |
)
|
112 |
|
113 |
def encode_video_qwen(self, video_path):
|
114 |
-
"""Qwen API용 비디오 인코딩"""
|
115 |
with open(video_path, "rb") as video_file:
|
116 |
return base64.b64encode(video_file.read()).decode("utf-8")
|
117 |
|
118 |
|
119 |
-
#
|
120 |
-
# VideoProcessor: 비디오 처리 및 프레임 추출 기능 제공 (변경 없음)
|
121 |
-
# -----------------------------------------------------------------------------
|
122 |
class VideoProcessor:
|
123 |
-
"""비디오 처리 및 프레임 추출 기능 제공"""
|
124 |
-
|
125 |
def extract_video_frames(self, video_path, output_folder=None, fps=1):
|
126 |
-
"""비디오 파일에서 프레임 추출"""
|
127 |
if not video_path:
|
128 |
return [], None
|
129 |
-
|
130 |
if output_folder is None:
|
131 |
output_folder = f"frames_list/frames_{uuid.uuid4().hex}"
|
132 |
-
|
133 |
os.makedirs(output_folder, exist_ok=True)
|
134 |
cap = cv2.VideoCapture(video_path)
|
135 |
-
|
136 |
if not cap.isOpened():
|
137 |
print(f"오류: 비디오 파일을 열 수 없습니다 - {video_path}")
|
138 |
return [], None
|
139 |
-
|
140 |
frame_paths = []
|
141 |
frame_rate = cap.get(cv2.CAP_PROP_FPS)
|
142 |
-
|
143 |
if not frame_rate or frame_rate == 0:
|
144 |
print("경고: FPS를 읽을 수 없습니다, 기본값 4으로 설정합니다.")
|
145 |
frame_rate = 4.0
|
146 |
-
|
147 |
frame_interval = int(frame_rate / fps) if fps > 0 else 1
|
148 |
if frame_interval <= 0:
|
149 |
frame_interval = 1
|
150 |
-
|
151 |
frame_count = 0
|
152 |
saved_frame_count = 0
|
153 |
-
|
154 |
while cap.isOpened():
|
155 |
ret, frame = cap.read()
|
156 |
if not ret:
|
157 |
break
|
158 |
-
|
159 |
if frame is None:
|
160 |
print(f"경고: {frame_count}번째 프레임이 비어있습니다.")
|
161 |
frame_count += 1
|
162 |
continue
|
163 |
-
|
164 |
if frame_count % frame_interval == 0:
|
165 |
frame_path = os.path.join(output_folder, f"frame_{saved_frame_count}.jpg")
|
166 |
try:
|
@@ -171,28 +139,22 @@ class VideoProcessor:
|
|
171 |
print(f"경고: {frame_path} 저장 실패.")
|
172 |
except Exception as e:
|
173 |
print(f"경고: 프레임 저장 오류 ({frame_path}): {e}")
|
174 |
-
|
175 |
frame_count += 1
|
176 |
-
|
177 |
cap.release()
|
178 |
-
|
179 |
if not frame_paths:
|
180 |
print("경고: 프레임 ���출 실패.")
|
181 |
if os.path.exists(output_folder):
|
182 |
shutil.rmtree(output_folder)
|
183 |
return [], None
|
184 |
-
|
185 |
return frame_paths, output_folder
|
186 |
|
187 |
def cleanup_temp_files(self, video_path, frame_folder):
|
188 |
-
"""임시 비디오 파일 및 프레임 폴더 정리"""
|
189 |
if video_path and "temp_video_" in video_path and os.path.exists(video_path):
|
190 |
try:
|
191 |
os.remove(video_path)
|
192 |
print(f"임시 비디오 파일 삭제: {video_path}")
|
193 |
except OSError as e:
|
194 |
print(f"임시 비디오 파일 삭제 오류: {e}")
|
195 |
-
|
196 |
if frame_folder and os.path.exists(frame_folder):
|
197 |
try:
|
198 |
shutil.rmtree(frame_folder)
|
@@ -201,24 +163,18 @@ class VideoProcessor:
|
|
201 |
print(f"프레임 폴더 삭제 오류: {e}")
|
202 |
|
203 |
|
204 |
-
#
|
205 |
-
# TipCalculator: 팁 계산 핵심 로직 (알리바바 Qwen만 사용)
|
206 |
-
# -----------------------------------------------------------------------------
|
207 |
class TipCalculator:
|
208 |
-
"""팁 계산 핵심 로직 (Alibaba Qwen만 사용)"""
|
209 |
-
|
210 |
def __init__(self, config: Config, model_clients: ModelClients, video_processor: VideoProcessor):
|
211 |
self.config = config
|
212 |
self.model_clients = model_clients
|
213 |
self.video_processor = video_processor
|
214 |
|
215 |
def parse_llm_output(self, output_text):
|
216 |
-
"""LLM 출력을 파싱하여 팁 계산 결과 추출"""
|
217 |
analysis = "Analysis not found."
|
218 |
tip_percentage = 0.0
|
219 |
tip_amount = 0.0
|
220 |
total_bill = 0.0
|
221 |
-
|
222 |
analysis_match = re.search(r"Analysis:\s*(.*?)Tip Percentage:", output_text, re.DOTALL | re.IGNORECASE)
|
223 |
if analysis_match:
|
224 |
analysis = analysis_match.group(1).strip()
|
@@ -226,7 +182,6 @@ class TipCalculator:
|
|
226 |
analysis_match_alt = re.search(r"Analysis:\s*(.*)", output_text, re.DOTALL | re.IGNORECASE)
|
227 |
if analysis_match_alt:
|
228 |
analysis = analysis_match_alt.group(1).strip()
|
229 |
-
|
230 |
percentage_match = re.search(r"Tip Percentage:\s*\*{0,2}(\d+(?:\.\d+)?)%\*{0,2}", output_text,
|
231 |
re.DOTALL | re.IGNORECASE)
|
232 |
if percentage_match:
|
@@ -235,7 +190,6 @@ class TipCalculator:
|
|
235 |
except ValueError:
|
236 |
print(f"경고: Tip Percentage 변환 실패 - {percentage_match.group(1)}")
|
237 |
tip_percentage = 0.0
|
238 |
-
|
239 |
tip_match = re.search(r"Tip Amount:\s*\$?\s*([0-9.]+)", output_text, re.IGNORECASE)
|
240 |
if tip_match:
|
241 |
try:
|
@@ -245,32 +199,24 @@ class TipCalculator:
|
|
245 |
tip_amount = 0.0
|
246 |
else:
|
247 |
print(f"경고: 출력에서 Tip Amount를 찾을 수 없습니다:\n{output_text}")
|
248 |
-
|
249 |
total_match = re.search(r"Total Bill:\s*\$?\s*([0-9.]+)", output_text, re.IGNORECASE)
|
250 |
if total_match:
|
251 |
try:
|
252 |
total_bill = float(total_match.group(1))
|
253 |
except ValueError:
|
254 |
print(f"경고: Total Bill 변환 실패 - {total_match.group(1)}")
|
255 |
-
|
256 |
if len(analysis) < 20 and analysis == "Analysis not found.":
|
257 |
analysis = output_text
|
258 |
-
|
259 |
return analysis, tip_percentage, tip_amount, output_text
|
260 |
|
261 |
def process_tip_qwen(self, video_file_path, star_rating, user_review, calculated_subtotal, custom_prompt=None):
|
262 |
-
"""Qwen API를 사용한 팁 계산 처리 (비디오 캡션 생성 및 팁 산출)"""
|
263 |
if not os.path.exists(video_file_path):
|
264 |
return "Error: 비디오 파일 경로가 유효하지 않습니다.", 0.0, 0.0, [], None, ""
|
265 |
-
|
266 |
-
# 비디오 -> base64 인코딩
|
267 |
base64_video = self.model_clients.encode_video_qwen(video_file_path)
|
268 |
-
# Omni 프롬프트
|
269 |
omni_caption_prompt = '''
|
270 |
Task 1: Describe the waiters' actions in these restaurant video frames. Please check for mistakes or negative behaviors.
|
271 |
Task 2: Provide a short chronological summary of the entire scene.
|
272 |
'''
|
273 |
-
# Omni 스트리밍 호출
|
274 |
omni_result = self.model_clients.qwen_client.chat.completions.create(
|
275 |
model="qwen2.5-omni-7b",
|
276 |
messages=[
|
@@ -281,10 +227,7 @@ Task 2: Provide a short chronological summary of the entire scene.
|
|
281 |
{
|
282 |
"role": "user",
|
283 |
"content": [
|
284 |
-
{
|
285 |
-
"type": "video_url",
|
286 |
-
"video_url": {"url": f"data:;base64,{base64_video}"},
|
287 |
-
},
|
288 |
{"type": "text", "text": omni_caption_prompt},
|
289 |
],
|
290 |
},
|
@@ -293,7 +236,6 @@ Task 2: Provide a short chronological summary of the entire scene.
|
|
293 |
stream=True,
|
294 |
stream_options={"include_usage": True},
|
295 |
)
|
296 |
-
# 캡션 추출
|
297 |
all_omni_chunks = list(omni_result)
|
298 |
caption_text = ""
|
299 |
for chunk in all_omni_chunks[:-1]:
|
@@ -303,8 +245,6 @@ Task 2: Provide a short chronological summary of the entire scene.
|
|
303 |
caption_text += chunk.choices[0].delta.content
|
304 |
if not caption_text.strip():
|
305 |
caption_text = "(No caption from Omni)"
|
306 |
-
|
307 |
-
# --- 2) qvq-max로 팁 계산 ---
|
308 |
user_review = user_review.strip() if user_review else "(No user review)"
|
309 |
if custom_prompt is None:
|
310 |
prompt = self.config.DEFAULT_PROMPT_TEMPLATE.format(
|
@@ -325,22 +265,12 @@ Task 2: Provide a short chronological summary of the entire scene.
|
|
325 |
star_rating=star_rating,
|
326 |
user_review=user_review
|
327 |
)
|
328 |
-
|
329 |
final_prompt = prompt.replace("{caption_text}", caption_text)
|
330 |
-
|
331 |
qvq_result = self.model_clients.qwen_client.chat.completions.create(
|
332 |
model="qwen2.5-vl-32b-instruct",
|
333 |
messages=[
|
334 |
-
{
|
335 |
-
|
336 |
-
"content": [{"type": "text", "text": "You are a helpful assistant."}],
|
337 |
-
},
|
338 |
-
{
|
339 |
-
"role": "user",
|
340 |
-
"content": [
|
341 |
-
{"type": "text", "text": final_prompt}
|
342 |
-
],
|
343 |
-
},
|
344 |
],
|
345 |
modalities=["text"],
|
346 |
stream=True,
|
@@ -360,13 +290,11 @@ Task 2: Provide a short chronological summary of the entire scene.
|
|
360 |
print("\n" + "=" * 20 + "Complete Response" + "=" * 20 + "\n")
|
361 |
is_answering = True
|
362 |
final_answer += d.content
|
363 |
-
|
364 |
final_text = final_reasoning + "\n" + final_answer
|
365 |
analysis, tip_percentage, tip_amount, output_text = self.parse_llm_output(final_text)
|
366 |
return analysis, tip_percentage, tip_amount, [], None, output_text
|
367 |
|
368 |
def calculate_manual_tip(self, tip_percent, subtotal):
|
369 |
-
"""백분율에 따른 수동 팁 계산"""
|
370 |
tip_amount = subtotal * (tip_percent / 100)
|
371 |
total_bill = subtotal + tip_amount
|
372 |
analysis_output = f"Manual calculation using fixed tip percentage of {tip_percent}%."
|
@@ -375,51 +303,38 @@ Task 2: Provide a short chronological summary of the entire scene.
|
|
375 |
return analysis_output, tip_output, total_bill_output
|
376 |
|
377 |
|
378 |
-
#
|
379 |
-
# UIHandler: Gradio UI 이벤트 및 콜백 처리 (Qwen만 사용, 알리바바 API 키 입력 필드 추가)
|
380 |
-
# -----------------------------------------------------------------------------
|
381 |
class UIHandler:
|
382 |
-
"""Gradio UI 이벤트 및 콜백 처리"""
|
383 |
-
|
384 |
def __init__(self, config: Config, tip_calculator: TipCalculator, video_processor: VideoProcessor):
|
385 |
self.config = config
|
386 |
self.tip_calculator = tip_calculator
|
387 |
self.video_processor = video_processor
|
388 |
|
389 |
def update_subtotal_and_prompt(self, *args):
|
390 |
-
"""사용자 입력에 따라 소계 및 프롬프트 업데이트"""
|
391 |
num_food_items = len(self.config.FOOD_ITEMS)
|
392 |
quantities = args[:num_food_items]
|
393 |
star_rating = args[num_food_items]
|
394 |
user_review = args[num_food_items + 1]
|
395 |
-
|
396 |
calculated_subtotal = 0.0
|
397 |
for i in range(num_food_items):
|
398 |
calculated_subtotal += self.config.FOOD_ITEMS[i]['price'] * quantities[i]
|
399 |
-
|
400 |
user_review_text = user_review.strip() if user_review and user_review.strip() else "(No user review provided)"
|
401 |
-
|
402 |
updated_prompt = self.config.DEFAULT_PROMPT_TEMPLATE.format(
|
403 |
calculated_subtotal=calculated_subtotal,
|
404 |
star_rating=star_rating,
|
405 |
user_review=user_review_text
|
406 |
)
|
407 |
updated_prompt = updated_prompt.replace("{caption_text}", "{{caption_text}}")
|
408 |
-
|
409 |
return calculated_subtotal, updated_prompt
|
410 |
|
411 |
def compute_tip(self, alibaba_key, video_file_obj, subtotal, star_rating, user_review, custom_prompt_text):
|
412 |
-
"""알리바바 Qwen 모델을 사용하여 팁 계산"""
|
413 |
analysis_output = "계산을 시작합니다..."
|
414 |
tip_percentage = 0.0
|
415 |
tip_output = "$0.00"
|
416 |
total_bill_output = f"${subtotal:.2f}"
|
417 |
-
|
418 |
if video_file_obj is None:
|
419 |
return "오류: 비디오 파일을 업로드해주세요.", "$0.00", total_bill_output, custom_prompt_text, gr.update(value=None)
|
420 |
-
|
421 |
try:
|
422 |
-
# 입력받은 알리바바 API 키가 있으면 Qwen 클라이언트를 재설정
|
423 |
if alibaba_key and alibaba_key.strip():
|
424 |
from openai import OpenAI as QwenOpenAI
|
425 |
self.tip_calculator.model_clients.qwen_client = QwenOpenAI(
|
@@ -433,7 +348,6 @@ class UIHandler:
|
|
433 |
except Exception as e:
|
434 |
print(f"임시 비디오 파일 생성 오류: {e}")
|
435 |
return f"오류: 비디오 파일을 처리할 수 없습니다: {e}", "$0.00", total_bill_output, custom_prompt_text, None
|
436 |
-
|
437 |
frame_folder = None
|
438 |
try:
|
439 |
analysis, tip_percentage, tip_amount, _, _, output_text = self.tip_calculator.process_tip_qwen(
|
@@ -454,11 +368,9 @@ class UIHandler:
|
|
454 |
total_bill_output = f"${subtotal:.2f}"
|
455 |
finally:
|
456 |
self.video_processor.cleanup_temp_files(temp_video_path, frame_folder)
|
457 |
-
|
458 |
return analysis_output, tip_output, total_bill_output, custom_prompt_text, gr.update(value=None)
|
459 |
|
460 |
def auto_tip_and_invoice(self, alibaba_key, video_file_obj, subtotal, star_rating, review, prompt, *quantities):
|
461 |
-
"""AI 모델을 사용한 자동 팁 계산 및 청구서 업데이트 (알리바바 Qwen만 사용)"""
|
462 |
analysis, tip_disp, total_bill_disp, prompt_out, vid_out = self.compute_tip(
|
463 |
alibaba_key, video_file_obj, subtotal, star_rating, review, prompt
|
464 |
)
|
@@ -466,72 +378,54 @@ class UIHandler:
|
|
466 |
return analysis, tip_disp, total_bill_disp, prompt_out, vid_out, invoice
|
467 |
|
468 |
def update_invoice_summary(self, *args):
|
469 |
-
"""수량 및 팁에 따라 청구서 요약 업데이트"""
|
470 |
num_items = len(self.config.FOOD_ITEMS)
|
471 |
quantities = args[:num_items]
|
472 |
-
|
473 |
if len(args) >= num_items + 2:
|
474 |
tip_str = args[num_items]
|
475 |
total_bill_str = args[num_items + 1]
|
476 |
else:
|
477 |
tip_str = "$0.00"
|
478 |
total_bill_str = "$0.00"
|
479 |
-
|
480 |
summary = ""
|
481 |
for i, q in enumerate(quantities):
|
482 |
try:
|
483 |
q_val = float(q)
|
484 |
except:
|
485 |
q_val = 0
|
486 |
-
|
487 |
if q_val > 0:
|
488 |
item = self.config.FOOD_ITEMS[i]
|
489 |
total_price = item['price'] * q_val
|
490 |
summary += f"{item['name']} x{int(q_val)} : ${total_price:.2f}\n"
|
491 |
-
|
492 |
if summary == "":
|
493 |
summary = "주문한 메뉴가 없습니다."
|
494 |
-
|
495 |
summary += f"\nTip: {tip_str}\nTotal Bill: {total_bill_str}"
|
496 |
-
|
497 |
return summary
|
498 |
|
499 |
def manual_tip_and_invoice(self, tip_percent, subtotal, *quantities):
|
500 |
-
"""수동 팁 계산 및 청구서 업데이트"""
|
501 |
analysis, tip_disp, total_bill_disp = self.tip_calculator.calculate_manual_tip(tip_percent, subtotal)
|
502 |
invoice = self.update_invoice_summary(*quantities, tip_disp, total_bill_disp)
|
503 |
return analysis, tip_disp, total_bill_disp, invoice
|
504 |
|
505 |
def process_payment(self, total_bill):
|
506 |
-
"""총 청구액에 대한 결제 처리"""
|
507 |
return f"{total_bill} 결제되었습니다."
|
508 |
|
509 |
|
510 |
-
#
|
511 |
-
# App: 모든 것을 연결하는 메인 애플리케이션 클래스
|
512 |
-
# -----------------------------------------------------------------------------
|
513 |
class App:
|
514 |
-
"""메인 애플리케이션 클래스"""
|
515 |
-
|
516 |
def __init__(self):
|
517 |
self.config = Config()
|
518 |
self.model_clients = ModelClients(self.config)
|
519 |
self.video_processor = VideoProcessor()
|
520 |
self.tip_calculator = TipCalculator(self.config, self.model_clients, self.video_processor)
|
521 |
self.ui_handler = UIHandler(self.config, self.tip_calculator, self.video_processor)
|
522 |
-
|
523 |
-
# Flask 앱 (필요 시 사용)
|
524 |
self.flask_app = Flask(__name__)
|
525 |
|
526 |
def create_gradio_blocks(self):
|
527 |
-
"""Gradio Blocks 인터페이스 구성"""
|
528 |
with gr.Blocks(title="Video Tip Calculation Interface", theme=gr.themes.Soft(),
|
529 |
css=self.config.CUSTOM_CSS) as interface:
|
530 |
gr.Markdown("## Video Tip Calculation Interface (Structured)")
|
531 |
-
|
532 |
quantity_inputs = []
|
533 |
subtotal_display = gr.Number(label="Subtotal ($)", value=0.0, interactive=False, visible=False)
|
534 |
-
|
535 |
with gr.Row():
|
536 |
with gr.Column(scale=2):
|
537 |
gr.Markdown("### 1. Select Food Items")
|
@@ -556,13 +450,10 @@ class App:
|
|
556 |
elem_id=f"qty_{item['name'].replace(' ', '_')}"
|
557 |
)
|
558 |
quantity_inputs.append(q_input)
|
559 |
-
|
560 |
subtotal_visible_display = gr.Textbox(label="Subtotal", value="$0.00", interactive=False)
|
561 |
-
|
562 |
gr.Markdown("### 2. Service Feedback")
|
563 |
review_input = gr.Textbox(label="Review", placeholder="서비스 리뷰를 작성해주세요.", lines=3)
|
564 |
rating_input = gr.Radio(choices=[1, 2, 3, 4, 5], value=3, label="⭐Star Rating (1-5)⭐", type="value")
|
565 |
-
|
566 |
gr.Markdown("### 3. Calculate Tip")
|
567 |
with gr.Row():
|
568 |
btn_5 = gr.Button("5%")
|
@@ -570,21 +461,15 @@ class App:
|
|
570 |
btn_15 = gr.Button("15%")
|
571 |
btn_20 = gr.Button("20%")
|
572 |
btn_25 = gr.Button("25%")
|
573 |
-
|
574 |
-
# Qwen 모델 버튼만 남김
|
575 |
with gr.Row():
|
576 |
qwen_btn = gr.Button("Alibaba-Qwen", variant="tertiary", elem_id="qwen-button")
|
577 |
-
|
578 |
gr.Markdown("### 4. Results")
|
579 |
tip_display = gr.Textbox(label="Calculated Tip", value="$0.00", interactive=False)
|
580 |
-
total_bill_display = gr.Textbox(label="Total Bill (Subtotal + Tip)", value="$0.00",
|
581 |
-
interactive=False)
|
582 |
payment_btn = gr.Button("결제하기")
|
583 |
payment_result = gr.Textbox(label="Payment Result", value="", interactive=False)
|
584 |
-
|
585 |
with gr.Column(scale=1):
|
586 |
gr.Markdown("### 5. Upload & Prompt")
|
587 |
-
# 알리바바 API 키 입력 필드 추가
|
588 |
alibaba_key_input = gr.Textbox(label="Alibaba API Key", placeholder="Enter your Alibaba API Key", lines=1)
|
589 |
video_input = gr.Video(label="Upload Service Video")
|
590 |
prompt_display = gr.Textbox(
|
@@ -598,21 +483,15 @@ class App:
|
|
598 |
user_review="(No user review provided)"
|
599 |
).replace("{caption_text}", "{{caption_text}}")
|
600 |
)
|
601 |
-
|
602 |
gr.Markdown("### 6. AI Analysis")
|
603 |
analysis_display = gr.Textbox(label="AI Analysis", lines=10, max_lines=15, interactive=True)
|
604 |
-
|
605 |
gr.Markdown("### 7. 청구서")
|
606 |
order_summary_display = gr.Textbox(label="청구서", value="주문한 메뉴가 없습니다.", interactive=True)
|
607 |
-
|
608 |
-
# Subtotal 값 업데이트 시 $ 표시 갱신
|
609 |
subtotal_display.change(
|
610 |
fn=lambda x: f"${x:.2f}",
|
611 |
inputs=[subtotal_display],
|
612 |
outputs=[subtotal_visible_display]
|
613 |
)
|
614 |
-
|
615 |
-
# 음식 수량, 별점, 리뷰가 바뀔 때마다 Subtotal, Prompt 업데이트
|
616 |
inputs_for_prompt_update = quantity_inputs + [rating_input, review_input]
|
617 |
outputs_for_prompt_update = [subtotal_display, prompt_display]
|
618 |
for comp in inputs_for_prompt_update:
|
@@ -621,21 +500,16 @@ class App:
|
|
621 |
inputs=inputs_for_prompt_update,
|
622 |
outputs=outputs_for_prompt_update
|
623 |
)
|
624 |
-
|
625 |
-
# 수량 변화 시 청구서 텍스트 업데이트
|
626 |
for comp in quantity_inputs:
|
627 |
comp.change(
|
628 |
fn=self.ui_handler.update_invoice_summary,
|
629 |
inputs=quantity_inputs,
|
630 |
outputs=order_summary_display
|
631 |
)
|
632 |
-
|
633 |
-
# 모델 호출 후 결과 업데이트 (알리바바 API 키 포함)
|
634 |
compute_inputs = [alibaba_key_input, video_input, subtotal_display, rating_input, review_input, prompt_display] + quantity_inputs
|
635 |
compute_outputs = [
|
636 |
analysis_display, tip_display, total_bill_display, prompt_display, video_input, order_summary_display
|
637 |
]
|
638 |
-
|
639 |
qwen_btn.click(
|
640 |
fn=lambda alibaba_key, vid, sub, rat, rev, prom, *qty: self.ui_handler.auto_tip_and_invoice(
|
641 |
alibaba_key, vid, sub, rat, rev, prom, *qty
|
@@ -643,8 +517,6 @@ class App:
|
|
643 |
inputs=compute_inputs,
|
644 |
outputs=compute_outputs
|
645 |
)
|
646 |
-
|
647 |
-
# 수동 팁 계산 버튼들
|
648 |
btn_5.click(
|
649 |
fn=lambda sub, *qty: self.ui_handler.manual_tip_and_invoice(5, sub, *qty),
|
650 |
inputs=[subtotal_display] + quantity_inputs,
|
@@ -670,35 +542,24 @@ class App:
|
|
670 |
inputs=[subtotal_display] + quantity_inputs,
|
671 |
outputs=[analysis_display, tip_display, total_bill_display, order_summary_display]
|
672 |
)
|
673 |
-
|
674 |
-
# 결제 버튼
|
675 |
payment_btn.click(
|
676 |
fn=self.ui_handler.process_payment,
|
677 |
inputs=[total_bill_display],
|
678 |
outputs=[payment_result]
|
679 |
)
|
680 |
-
|
681 |
return interface
|
682 |
|
683 |
def run_gradio(self):
|
684 |
-
"""Gradio 서버 실행"""
|
685 |
interface = self.create_gradio_blocks()
|
686 |
interface.launch(share=True)
|
687 |
|
688 |
def run_flask(self):
|
689 |
-
"""Flask 서버 실행 (원한다면)"""
|
690 |
-
|
691 |
@self.flask_app.route("/")
|
692 |
def index():
|
693 |
return "Hello Flask"
|
694 |
-
|
695 |
self.flask_app.run(host="0.0.0.0", port=5000, debug=True)
|
696 |
|
697 |
|
698 |
if __name__ == "__main__":
|
699 |
app = App()
|
700 |
-
# Gradio UI 실행
|
701 |
app.run_gradio()
|
702 |
-
|
703 |
-
# Flask 서버 실행 (원하면 아래 주석 해제)
|
704 |
-
# app.run_flask()
|
|
|
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:
|
12 |
"""애플리케이션 설정 및 상수"""
|
|
|
|
|
13 |
FOOD_ITEMS = [
|
14 |
{"name": "짜장면", "image": "images/food1.jpg", "price": 7.00},
|
15 |
{"name": "짬뽕", "image": "images/food2.jpg", "price": 8.50},
|
|
|
20 |
{"name": "콜라", "image": "images/food6.jpg", "price": 12.00},
|
21 |
{"name": "사이다", "image": "images/food6.jpg", "price": 12.00},
|
22 |
]
|
|
|
23 |
# 알리바바 Qwen API 키 (기본값은 빈 문자열)
|
24 |
QWEN_API_KEY = ""
|
25 |
|
|
|
26 |
DEFAULT_PROMPT_TEMPLATE = (
|
27 |
"### Persona ###\n"
|
28 |
"You are an expert tip calculation assistant focusing on service quality observed in a video.\n\n"
|
|
|
58 |
"Total Bill: $[Subtotal + Tip]"
|
59 |
)
|
60 |
|
|
|
61 |
CUSTOM_CSS = """
|
62 |
#food-container {
|
63 |
display: grid;
|
|
|
66 |
overflow-y: auto;
|
67 |
height: 600px;
|
68 |
}
|
|
|
|
|
69 |
#qwen-button {
|
70 |
background-color: #8A2BE2 !important;
|
71 |
color: white !important;
|
72 |
border-color: #8A2BE2 !important;
|
73 |
}
|
|
|
74 |
#qwen-button:hover {
|
75 |
background-color: #7722CC !important;
|
76 |
}
|
77 |
"""
|
78 |
|
79 |
def __init__(self):
|
|
|
80 |
if not os.path.exists("images"):
|
81 |
print("경고: 'images' 폴더를 찾을 수 없습니다. 음식 이미지가 표시되지 않을 수 있습니다.")
|
82 |
for item in self.FOOD_ITEMS:
|
|
|
84 |
print(f"경고: 이미지 파일을 찾을 수 없습니다 - {item['image']}")
|
85 |
|
86 |
|
87 |
+
# --- ModelClients (알리바바 Qwen API만 사용) ---
|
|
|
|
|
88 |
class ModelClients:
|
|
|
|
|
89 |
def __init__(self, config: Config):
|
90 |
self.config = config
|
91 |
from openai import OpenAI as QwenOpenAI
|
|
|
95 |
)
|
96 |
|
97 |
def encode_video_qwen(self, video_path):
|
|
|
98 |
with open(video_path, "rb") as video_file:
|
99 |
return base64.b64encode(video_file.read()).decode("utf-8")
|
100 |
|
101 |
|
102 |
+
# --- VideoProcessor: 비디오 프레임 추출 ---
|
|
|
|
|
103 |
class VideoProcessor:
|
|
|
|
|
104 |
def extract_video_frames(self, video_path, output_folder=None, fps=1):
|
|
|
105 |
if not video_path:
|
106 |
return [], None
|
|
|
107 |
if output_folder is None:
|
108 |
output_folder = f"frames_list/frames_{uuid.uuid4().hex}"
|
|
|
109 |
os.makedirs(output_folder, exist_ok=True)
|
110 |
cap = cv2.VideoCapture(video_path)
|
|
|
111 |
if not cap.isOpened():
|
112 |
print(f"오류: 비디오 파일을 열 수 없습니다 - {video_path}")
|
113 |
return [], None
|
|
|
114 |
frame_paths = []
|
115 |
frame_rate = cap.get(cv2.CAP_PROP_FPS)
|
|
|
116 |
if not frame_rate or frame_rate == 0:
|
117 |
print("경고: FPS를 읽을 수 없습니다, 기본값 4으로 설정합니다.")
|
118 |
frame_rate = 4.0
|
|
|
119 |
frame_interval = int(frame_rate / fps) if fps > 0 else 1
|
120 |
if frame_interval <= 0:
|
121 |
frame_interval = 1
|
|
|
122 |
frame_count = 0
|
123 |
saved_frame_count = 0
|
|
|
124 |
while cap.isOpened():
|
125 |
ret, frame = cap.read()
|
126 |
if not ret:
|
127 |
break
|
|
|
128 |
if frame is None:
|
129 |
print(f"경고: {frame_count}번째 프레임이 비어있습니다.")
|
130 |
frame_count += 1
|
131 |
continue
|
|
|
132 |
if frame_count % frame_interval == 0:
|
133 |
frame_path = os.path.join(output_folder, f"frame_{saved_frame_count}.jpg")
|
134 |
try:
|
|
|
139 |
print(f"경고: {frame_path} 저장 실패.")
|
140 |
except Exception as e:
|
141 |
print(f"경고: 프레임 저장 오류 ({frame_path}): {e}")
|
|
|
142 |
frame_count += 1
|
|
|
143 |
cap.release()
|
|
|
144 |
if not frame_paths:
|
145 |
print("경고: 프레임 ���출 실패.")
|
146 |
if os.path.exists(output_folder):
|
147 |
shutil.rmtree(output_folder)
|
148 |
return [], None
|
|
|
149 |
return frame_paths, output_folder
|
150 |
|
151 |
def cleanup_temp_files(self, video_path, frame_folder):
|
|
|
152 |
if video_path and "temp_video_" in video_path and os.path.exists(video_path):
|
153 |
try:
|
154 |
os.remove(video_path)
|
155 |
print(f"임시 비디오 파일 삭제: {video_path}")
|
156 |
except OSError as e:
|
157 |
print(f"임시 비디오 파일 삭제 오류: {e}")
|
|
|
158 |
if frame_folder and os.path.exists(frame_folder):
|
159 |
try:
|
160 |
shutil.rmtree(frame_folder)
|
|
|
163 |
print(f"프레임 폴더 삭제 오류: {e}")
|
164 |
|
165 |
|
166 |
+
# --- TipCalculator (알리바바 Qwen API를 사용한 팁 계산) ---
|
|
|
|
|
167 |
class TipCalculator:
|
|
|
|
|
168 |
def __init__(self, config: Config, model_clients: ModelClients, video_processor: VideoProcessor):
|
169 |
self.config = config
|
170 |
self.model_clients = model_clients
|
171 |
self.video_processor = video_processor
|
172 |
|
173 |
def parse_llm_output(self, output_text):
|
|
|
174 |
analysis = "Analysis not found."
|
175 |
tip_percentage = 0.0
|
176 |
tip_amount = 0.0
|
177 |
total_bill = 0.0
|
|
|
178 |
analysis_match = re.search(r"Analysis:\s*(.*?)Tip Percentage:", output_text, re.DOTALL | re.IGNORECASE)
|
179 |
if analysis_match:
|
180 |
analysis = analysis_match.group(1).strip()
|
|
|
182 |
analysis_match_alt = re.search(r"Analysis:\s*(.*)", output_text, re.DOTALL | re.IGNORECASE)
|
183 |
if analysis_match_alt:
|
184 |
analysis = analysis_match_alt.group(1).strip()
|
|
|
185 |
percentage_match = re.search(r"Tip Percentage:\s*\*{0,2}(\d+(?:\.\d+)?)%\*{0,2}", output_text,
|
186 |
re.DOTALL | re.IGNORECASE)
|
187 |
if percentage_match:
|
|
|
190 |
except ValueError:
|
191 |
print(f"경고: Tip Percentage 변환 실패 - {percentage_match.group(1)}")
|
192 |
tip_percentage = 0.0
|
|
|
193 |
tip_match = re.search(r"Tip Amount:\s*\$?\s*([0-9.]+)", output_text, re.IGNORECASE)
|
194 |
if tip_match:
|
195 |
try:
|
|
|
199 |
tip_amount = 0.0
|
200 |
else:
|
201 |
print(f"경고: 출력에서 Tip Amount를 찾을 수 없습니다:\n{output_text}")
|
|
|
202 |
total_match = re.search(r"Total Bill:\s*\$?\s*([0-9.]+)", output_text, re.IGNORECASE)
|
203 |
if total_match:
|
204 |
try:
|
205 |
total_bill = float(total_match.group(1))
|
206 |
except ValueError:
|
207 |
print(f"경고: Total Bill 변환 실패 - {total_match.group(1)}")
|
|
|
208 |
if len(analysis) < 20 and analysis == "Analysis not found.":
|
209 |
analysis = output_text
|
|
|
210 |
return analysis, tip_percentage, tip_amount, output_text
|
211 |
|
212 |
def process_tip_qwen(self, video_file_path, star_rating, user_review, calculated_subtotal, custom_prompt=None):
|
|
|
213 |
if not os.path.exists(video_file_path):
|
214 |
return "Error: 비디오 파일 경로가 유효하지 않습니다.", 0.0, 0.0, [], None, ""
|
|
|
|
|
215 |
base64_video = self.model_clients.encode_video_qwen(video_file_path)
|
|
|
216 |
omni_caption_prompt = '''
|
217 |
Task 1: Describe the waiters' actions in these restaurant video frames. Please check for mistakes or negative behaviors.
|
218 |
Task 2: Provide a short chronological summary of the entire scene.
|
219 |
'''
|
|
|
220 |
omni_result = self.model_clients.qwen_client.chat.completions.create(
|
221 |
model="qwen2.5-omni-7b",
|
222 |
messages=[
|
|
|
227 |
{
|
228 |
"role": "user",
|
229 |
"content": [
|
230 |
+
{"type": "video_url", "video_url": {"url": f"data:;base64,{base64_video}"}},
|
|
|
|
|
|
|
231 |
{"type": "text", "text": omni_caption_prompt},
|
232 |
],
|
233 |
},
|
|
|
236 |
stream=True,
|
237 |
stream_options={"include_usage": True},
|
238 |
)
|
|
|
239 |
all_omni_chunks = list(omni_result)
|
240 |
caption_text = ""
|
241 |
for chunk in all_omni_chunks[:-1]:
|
|
|
245 |
caption_text += chunk.choices[0].delta.content
|
246 |
if not caption_text.strip():
|
247 |
caption_text = "(No caption from Omni)"
|
|
|
|
|
248 |
user_review = user_review.strip() if user_review else "(No user review)"
|
249 |
if custom_prompt is None:
|
250 |
prompt = self.config.DEFAULT_PROMPT_TEMPLATE.format(
|
|
|
265 |
star_rating=star_rating,
|
266 |
user_review=user_review
|
267 |
)
|
|
|
268 |
final_prompt = prompt.replace("{caption_text}", caption_text)
|
|
|
269 |
qvq_result = self.model_clients.qwen_client.chat.completions.create(
|
270 |
model="qwen2.5-vl-32b-instruct",
|
271 |
messages=[
|
272 |
+
{"role": "system", "content": [{"type": "text", "text": "You are a helpful assistant."}]},
|
273 |
+
{"role": "user", "content": [{"type": "text", "text": final_prompt}]},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
274 |
],
|
275 |
modalities=["text"],
|
276 |
stream=True,
|
|
|
290 |
print("\n" + "=" * 20 + "Complete Response" + "=" * 20 + "\n")
|
291 |
is_answering = True
|
292 |
final_answer += d.content
|
|
|
293 |
final_text = final_reasoning + "\n" + final_answer
|
294 |
analysis, tip_percentage, tip_amount, output_text = self.parse_llm_output(final_text)
|
295 |
return analysis, tip_percentage, tip_amount, [], None, output_text
|
296 |
|
297 |
def calculate_manual_tip(self, tip_percent, subtotal):
|
|
|
298 |
tip_amount = subtotal * (tip_percent / 100)
|
299 |
total_bill = subtotal + tip_amount
|
300 |
analysis_output = f"Manual calculation using fixed tip percentage of {tip_percent}%."
|
|
|
303 |
return analysis_output, tip_output, total_bill_output
|
304 |
|
305 |
|
306 |
+
# --- UIHandler: Gradio 인터페이스 이벤트 처리 (알리바바 API 키 입력 포함) ---
|
|
|
|
|
307 |
class UIHandler:
|
|
|
|
|
308 |
def __init__(self, config: Config, tip_calculator: TipCalculator, video_processor: VideoProcessor):
|
309 |
self.config = config
|
310 |
self.tip_calculator = tip_calculator
|
311 |
self.video_processor = video_processor
|
312 |
|
313 |
def update_subtotal_and_prompt(self, *args):
|
|
|
314 |
num_food_items = len(self.config.FOOD_ITEMS)
|
315 |
quantities = args[:num_food_items]
|
316 |
star_rating = args[num_food_items]
|
317 |
user_review = args[num_food_items + 1]
|
|
|
318 |
calculated_subtotal = 0.0
|
319 |
for i in range(num_food_items):
|
320 |
calculated_subtotal += self.config.FOOD_ITEMS[i]['price'] * quantities[i]
|
|
|
321 |
user_review_text = user_review.strip() if user_review and user_review.strip() else "(No user review provided)"
|
|
|
322 |
updated_prompt = self.config.DEFAULT_PROMPT_TEMPLATE.format(
|
323 |
calculated_subtotal=calculated_subtotal,
|
324 |
star_rating=star_rating,
|
325 |
user_review=user_review_text
|
326 |
)
|
327 |
updated_prompt = updated_prompt.replace("{caption_text}", "{{caption_text}}")
|
|
|
328 |
return calculated_subtotal, updated_prompt
|
329 |
|
330 |
def compute_tip(self, alibaba_key, video_file_obj, subtotal, star_rating, user_review, custom_prompt_text):
|
|
|
331 |
analysis_output = "계산을 시작합니다..."
|
332 |
tip_percentage = 0.0
|
333 |
tip_output = "$0.00"
|
334 |
total_bill_output = f"${subtotal:.2f}"
|
|
|
335 |
if video_file_obj is None:
|
336 |
return "오류: 비디오 파일을 업로드해주세요.", "$0.00", total_bill_output, custom_prompt_text, gr.update(value=None)
|
|
|
337 |
try:
|
|
|
338 |
if alibaba_key and alibaba_key.strip():
|
339 |
from openai import OpenAI as QwenOpenAI
|
340 |
self.tip_calculator.model_clients.qwen_client = QwenOpenAI(
|
|
|
348 |
except Exception as e:
|
349 |
print(f"임시 비디오 파일 생성 오류: {e}")
|
350 |
return f"오류: 비디오 파일을 처리할 수 없습니다: {e}", "$0.00", total_bill_output, custom_prompt_text, None
|
|
|
351 |
frame_folder = None
|
352 |
try:
|
353 |
analysis, tip_percentage, tip_amount, _, _, output_text = self.tip_calculator.process_tip_qwen(
|
|
|
368 |
total_bill_output = f"${subtotal:.2f}"
|
369 |
finally:
|
370 |
self.video_processor.cleanup_temp_files(temp_video_path, frame_folder)
|
|
|
371 |
return analysis_output, tip_output, total_bill_output, custom_prompt_text, gr.update(value=None)
|
372 |
|
373 |
def auto_tip_and_invoice(self, alibaba_key, video_file_obj, subtotal, star_rating, review, prompt, *quantities):
|
|
|
374 |
analysis, tip_disp, total_bill_disp, prompt_out, vid_out = self.compute_tip(
|
375 |
alibaba_key, video_file_obj, subtotal, star_rating, review, prompt
|
376 |
)
|
|
|
378 |
return analysis, tip_disp, total_bill_disp, prompt_out, vid_out, invoice
|
379 |
|
380 |
def update_invoice_summary(self, *args):
|
|
|
381 |
num_items = len(self.config.FOOD_ITEMS)
|
382 |
quantities = args[:num_items]
|
|
|
383 |
if len(args) >= num_items + 2:
|
384 |
tip_str = args[num_items]
|
385 |
total_bill_str = args[num_items + 1]
|
386 |
else:
|
387 |
tip_str = "$0.00"
|
388 |
total_bill_str = "$0.00"
|
|
|
389 |
summary = ""
|
390 |
for i, q in enumerate(quantities):
|
391 |
try:
|
392 |
q_val = float(q)
|
393 |
except:
|
394 |
q_val = 0
|
|
|
395 |
if q_val > 0:
|
396 |
item = self.config.FOOD_ITEMS[i]
|
397 |
total_price = item['price'] * q_val
|
398 |
summary += f"{item['name']} x{int(q_val)} : ${total_price:.2f}\n"
|
|
|
399 |
if summary == "":
|
400 |
summary = "주문한 메뉴가 없습니다."
|
|
|
401 |
summary += f"\nTip: {tip_str}\nTotal Bill: {total_bill_str}"
|
|
|
402 |
return summary
|
403 |
|
404 |
def manual_tip_and_invoice(self, tip_percent, subtotal, *quantities):
|
|
|
405 |
analysis, tip_disp, total_bill_disp = self.tip_calculator.calculate_manual_tip(tip_percent, subtotal)
|
406 |
invoice = self.update_invoice_summary(*quantities, tip_disp, total_bill_disp)
|
407 |
return analysis, tip_disp, total_bill_disp, invoice
|
408 |
|
409 |
def process_payment(self, total_bill):
|
|
|
410 |
return f"{total_bill} 결제되었습니다."
|
411 |
|
412 |
|
413 |
+
# --- App: 모든 컴포넌트 연결 및 Gradio 인터페이스 실행 ---
|
|
|
|
|
414 |
class App:
|
|
|
|
|
415 |
def __init__(self):
|
416 |
self.config = Config()
|
417 |
self.model_clients = ModelClients(self.config)
|
418 |
self.video_processor = VideoProcessor()
|
419 |
self.tip_calculator = TipCalculator(self.config, self.model_clients, self.video_processor)
|
420 |
self.ui_handler = UIHandler(self.config, self.tip_calculator, self.video_processor)
|
|
|
|
|
421 |
self.flask_app = Flask(__name__)
|
422 |
|
423 |
def create_gradio_blocks(self):
|
|
|
424 |
with gr.Blocks(title="Video Tip Calculation Interface", theme=gr.themes.Soft(),
|
425 |
css=self.config.CUSTOM_CSS) as interface:
|
426 |
gr.Markdown("## Video Tip Calculation Interface (Structured)")
|
|
|
427 |
quantity_inputs = []
|
428 |
subtotal_display = gr.Number(label="Subtotal ($)", value=0.0, interactive=False, visible=False)
|
|
|
429 |
with gr.Row():
|
430 |
with gr.Column(scale=2):
|
431 |
gr.Markdown("### 1. Select Food Items")
|
|
|
450 |
elem_id=f"qty_{item['name'].replace(' ', '_')}"
|
451 |
)
|
452 |
quantity_inputs.append(q_input)
|
|
|
453 |
subtotal_visible_display = gr.Textbox(label="Subtotal", value="$0.00", interactive=False)
|
|
|
454 |
gr.Markdown("### 2. Service Feedback")
|
455 |
review_input = gr.Textbox(label="Review", placeholder="서비스 리뷰를 작성해주세요.", lines=3)
|
456 |
rating_input = gr.Radio(choices=[1, 2, 3, 4, 5], value=3, label="⭐Star Rating (1-5)⭐", type="value")
|
|
|
457 |
gr.Markdown("### 3. Calculate Tip")
|
458 |
with gr.Row():
|
459 |
btn_5 = gr.Button("5%")
|
|
|
461 |
btn_15 = gr.Button("15%")
|
462 |
btn_20 = gr.Button("20%")
|
463 |
btn_25 = gr.Button("25%")
|
|
|
|
|
464 |
with gr.Row():
|
465 |
qwen_btn = gr.Button("Alibaba-Qwen", variant="tertiary", elem_id="qwen-button")
|
|
|
466 |
gr.Markdown("### 4. Results")
|
467 |
tip_display = gr.Textbox(label="Calculated Tip", value="$0.00", interactive=False)
|
468 |
+
total_bill_display = gr.Textbox(label="Total Bill (Subtotal + Tip)", value="$0.00", interactive=False)
|
|
|
469 |
payment_btn = gr.Button("결제하기")
|
470 |
payment_result = gr.Textbox(label="Payment Result", value="", interactive=False)
|
|
|
471 |
with gr.Column(scale=1):
|
472 |
gr.Markdown("### 5. Upload & Prompt")
|
|
|
473 |
alibaba_key_input = gr.Textbox(label="Alibaba API Key", placeholder="Enter your Alibaba API Key", lines=1)
|
474 |
video_input = gr.Video(label="Upload Service Video")
|
475 |
prompt_display = gr.Textbox(
|
|
|
483 |
user_review="(No user review provided)"
|
484 |
).replace("{caption_text}", "{{caption_text}}")
|
485 |
)
|
|
|
486 |
gr.Markdown("### 6. AI Analysis")
|
487 |
analysis_display = gr.Textbox(label="AI Analysis", lines=10, max_lines=15, interactive=True)
|
|
|
488 |
gr.Markdown("### 7. 청구서")
|
489 |
order_summary_display = gr.Textbox(label="청구서", value="주문한 메뉴가 없습니다.", interactive=True)
|
|
|
|
|
490 |
subtotal_display.change(
|
491 |
fn=lambda x: f"${x:.2f}",
|
492 |
inputs=[subtotal_display],
|
493 |
outputs=[subtotal_visible_display]
|
494 |
)
|
|
|
|
|
495 |
inputs_for_prompt_update = quantity_inputs + [rating_input, review_input]
|
496 |
outputs_for_prompt_update = [subtotal_display, prompt_display]
|
497 |
for comp in inputs_for_prompt_update:
|
|
|
500 |
inputs=inputs_for_prompt_update,
|
501 |
outputs=outputs_for_prompt_update
|
502 |
)
|
|
|
|
|
503 |
for comp in quantity_inputs:
|
504 |
comp.change(
|
505 |
fn=self.ui_handler.update_invoice_summary,
|
506 |
inputs=quantity_inputs,
|
507 |
outputs=order_summary_display
|
508 |
)
|
|
|
|
|
509 |
compute_inputs = [alibaba_key_input, video_input, subtotal_display, rating_input, review_input, prompt_display] + quantity_inputs
|
510 |
compute_outputs = [
|
511 |
analysis_display, tip_display, total_bill_display, prompt_display, video_input, order_summary_display
|
512 |
]
|
|
|
513 |
qwen_btn.click(
|
514 |
fn=lambda alibaba_key, vid, sub, rat, rev, prom, *qty: self.ui_handler.auto_tip_and_invoice(
|
515 |
alibaba_key, vid, sub, rat, rev, prom, *qty
|
|
|
517 |
inputs=compute_inputs,
|
518 |
outputs=compute_outputs
|
519 |
)
|
|
|
|
|
520 |
btn_5.click(
|
521 |
fn=lambda sub, *qty: self.ui_handler.manual_tip_and_invoice(5, sub, *qty),
|
522 |
inputs=[subtotal_display] + quantity_inputs,
|
|
|
542 |
inputs=[subtotal_display] + quantity_inputs,
|
543 |
outputs=[analysis_display, tip_display, total_bill_display, order_summary_display]
|
544 |
)
|
|
|
|
|
545 |
payment_btn.click(
|
546 |
fn=self.ui_handler.process_payment,
|
547 |
inputs=[total_bill_display],
|
548 |
outputs=[payment_result]
|
549 |
)
|
|
|
550 |
return interface
|
551 |
|
552 |
def run_gradio(self):
|
|
|
553 |
interface = self.create_gradio_blocks()
|
554 |
interface.launch(share=True)
|
555 |
|
556 |
def run_flask(self):
|
|
|
|
|
557 |
@self.flask_app.route("/")
|
558 |
def index():
|
559 |
return "Hello Flask"
|
|
|
560 |
self.flask_app.run(host="0.0.0.0", port=5000, debug=True)
|
561 |
|
562 |
|
563 |
if __name__ == "__main__":
|
564 |
app = App()
|
|
|
565 |
app.run_gradio()
|
|
|
|
|
|
requirements.txt
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
gradio
|
2 |
+
opencv-python
|
3 |
+
flask
|
4 |
+
openai
|
space.yaml
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
runtime: python
|
2 |
+
python_version: "3.10"
|
3 |
+
hardware:
|
4 |
+
gpu: false
|