실행하려면
pip install pygame SpeechRecognition pyaudio pyttsx3 후 이 코드를 .py 파일로 저장해서 실행하세요.
코드는 voice-demo-code.py가 수정될 때마다 이 페이지에도 그대로 반영됩니다.
import pygame
import speech_recognition as sr
import pyttsx3
# ---------- 기본 설정값 ----------
W, H = 1000, 650
BG, INK, SUB = (253, 248, 240), (26, 16, 8), (120, 100, 80)
CARD, LINE = (255, 255, 255), (176, 96, 16)
SELECTED = (255, 224, 150) # 선택된 메뉴 버튼 색
DONE_GREEN = (10, 122, 48)
REC_RED = (200, 40, 40) # "듣고 있어요" 표시 색
BURGER_MENU = ["치킨버거", "불고기버거"]
SIDE_MENU = ["감자튀김", "어니언링", "치즈스틱"]
DRINK_MENU = ["콜라", "사이다", "환타"]
AUTO_LISTEN_MS = 10000 # 이 시간(1000분의 1초) 동안 터치가 없으면 음성 주문 시작
# ---------- 레벤슈타인 거리로 메뉴 이름 맞추기 (voice-guide 4번 챕터와 동일) ----------
def levenshtein(a, b):
n, m = len(a), len(b)
table = [[0] * (m + 1) for _ in range(n + 1)]
for i in range(n + 1):
table[i][0] = i
for j in range(m + 1):
table[0][j] = j
for i in range(1, n + 1):
for j in range(1, m + 1):
cost = 0 if a[i - 1] == b[j - 1] else 1
table[i][j] = min(
table[i - 1][j] + 1,
table[i][j - 1] + 1,
table[i - 1][j - 1] + cost
)
return table[n][m]
def find_best_menu(text, menu_list):
"""text와 가장 비슷한 메뉴 이름과, 그때의 편집 거리를 돌려준다."""
best_name = None
best_dist = None
for name in menu_list:
d = levenshtein(text, name)
if best_dist is None or d < best_dist:
best_dist = d
best_name = name
return best_name, best_dist
# ---------- 음성 인식 (voice-guide 2·3번 챕터와 동일) ----------
recognizer = sr.Recognizer()
def listen_and_recognize():
"""마이크로 한 번 듣고, 알아들은 문장을 돌려준다. 못 들으면 빈 문자열."""
with sr.Microphone() as source:
recognizer.adjust_for_ambient_noise(source, duration=0.6) # 주변 소음 줄이기
try:
audio = recognizer.listen(source, timeout=5, phrase_time_limit=6)
except sr.WaitTimeoutError:
return ""
try:
return recognizer.recognize_google(audio, language="ko-KR")
except sr.UnknownValueError:
return ""
except sr.RequestError:
return ""
# ---------- TTS(글자를 소리로 읽어주기) ----------
def speak(text):
engine = pyttsx3.init() # 매번 새로 만들어야 두 번째 이후에도 소리가 잘 난다
engine.say(text)
engine.runAndWait()
engine.stop()
# ---------- 화면 준비 ----------
pygame.init()
screen = pygame.display.set_mode((W, H))
pygame.display.set_caption("시니어 음성 주문 키오스크")
font = pygame.font.SysFont("malgungothic", 26)
small_font = pygame.font.SysFont("malgungothic", 18)
big_font = pygame.font.SysFont("malgungothic", 34)
clock = pygame.time.Clock()
cart = {"버거": None, "사이드": None, "음료": None}
state = "menu" # "menu"(터치로 고르기) -> "cart"(장바구니) -> "done"(결제 완료)
last_interaction = pygame.time.get_ticks() # 마지막으로 화면을 건드린 시각(ms)
# ---------- 메뉴 버튼 만들기 (챕터1과 똑같은 방식으로 3번 반복) ----------
burger_buttons = []
for i, name in enumerate(BURGER_MENU):
burger_buttons.append({"name": name, "rect": pygame.Rect(100 + i * 220, 140, 200, 100)})
side_buttons = []
for i, name in enumerate(SIDE_MENU):
side_buttons.append({"name": name, "rect": pygame.Rect(100 + i * 220, 280, 200, 100)})
drink_buttons = []
for i, name in enumerate(DRINK_MENU):
drink_buttons.append({"name": name, "rect": pygame.Rect(100 + i * 220, 420, 200, 100)})
pay_btn = pygame.Rect(W // 2 - 110, H - 100, 220, 60) # 장바구니 화면의 "카드 넣기" 버튼
restart_btn = pygame.Rect(W // 2 - 110, H - 100, 220, 60) # 완료 화면의 "처음으로" 버튼
# ---------- 화면 그리기 ----------
def draw_buttons(button_list, cart_key):
"""버튼 목록을 하나씩 그린다. 이미 고른 메뉴는 노란색으로 표시."""
for b in button_list:
color = SELECTED if cart[cart_key] == b["name"] else CARD
pygame.draw.rect(screen, color, b["rect"], border_radius=12)
pygame.draw.rect(screen, LINE, b["rect"], 3, border_radius=12)
label = font.render(b["name"], True, INK)
screen.blit(label, label.get_rect(center=b["rect"].center))
def draw_menu_screen():
"""터치 모드 기본 화면: 버거/사이드/음료를 한 번에 다 보여준다."""
screen.blit(font.render("버거", True, INK), (60, 100))
draw_buttons(burger_buttons, "버거")
screen.blit(font.render("사이드", True, INK), (60, 240))
draw_buttons(side_buttons, "사이드")
screen.blit(font.render("음료", True, INK), (60, 380))
draw_buttons(drink_buttons, "음료")
seconds_left = max(0, AUTO_LISTEN_MS // 1000 - (pygame.time.get_ticks() - last_interaction) // 1000)
timer_text = small_font.render(f"남은 시간 {seconds_left}초 (터치가 없으면 음성 안내가 시작돼요)", True, SUB)
screen.blit(timer_text, (60, 30))
def draw_cart_screen():
title = big_font.render("장바구니", True, INK)
screen.blit(title, (60, 60))
screen.blit(font.render(f"버거: {cart['버거']}", True, INK), (100, 160))
screen.blit(font.render(f"사이드: {cart['사이드']}", True, INK), (100, 220))
screen.blit(font.render(f"음료: {cart['음료']}", True, INK), (100, 280))
pygame.draw.rect(screen, DONE_GREEN, pay_btn, border_radius=12)
label = font.render("카드를 넣어주세요", True, (255, 255, 255))
screen.blit(label, label.get_rect(center=pay_btn.center))
def draw_done_screen():
title = big_font.render("주문이 완료됐습니다. 감사합니다!", True, DONE_GREEN)
screen.blit(title, title.get_rect(center=(W // 2, H // 2 - 40)))
pygame.draw.rect(screen, LINE, restart_btn, border_radius=12)
label = font.render("처음으로", True, (255, 255, 255))
screen.blit(label, label.get_rect(center=restart_btn.center))
# ---------- 음성으로 주문받기 (10초 동안 터치가 없으면 실행) ----------
def voice_order():
global cart, state
# 1. 버거 주문받기
screen.fill(BG)
screen.blit(small_font.render("버거를 골라주세요", True, SUB), (60, 30))
draw_buttons(burger_buttons, "버거")
pygame.display.flip()
speak("먼저 버거를 골라주세요. 드시고 싶은 버거를 말씀해주세요.")
screen.fill(BG)
screen.blit(small_font.render("듣고 있어요...", True, REC_RED), (60, 30))
draw_buttons(burger_buttons, "버거")
pygame.display.flip()
heard = listen_and_recognize()
name, dist = find_best_menu(heard, BURGER_MENU) if heard else (None, None)
if not name:
speak("다시 한 번 말씀해주세요")
heard = listen_and_recognize()
name, dist = find_best_menu(heard, BURGER_MENU) if heard else (None, None)
cart["버거"] = name if name else BURGER_MENU[0] # 그래도 못 들으면 첫 번째 메뉴로
screen.fill(BG)
screen.blit(small_font.render(f"선택하신 메뉴: {cart['버거']}", True, DONE_GREEN), (60, 30))
draw_buttons(burger_buttons, "버거")
pygame.display.flip()
pygame.time.delay(1200)
# 2. 사이드 주문받기 (버거와 똑같은 순서, 문구만 다름)
screen.fill(BG)
screen.blit(small_font.render("사이드를 골라주세요", True, SUB), (60, 30))
draw_buttons(side_buttons, "사이드")
pygame.display.flip()
speak("이번엔 사이드를 골라주세요. 드시고 싶은 사이드를 말씀해주세요.")
screen.fill(BG)
screen.blit(small_font.render("듣고 있어요...", True, REC_RED), (60, 30))
draw_buttons(side_buttons, "사이드")
pygame.display.flip()
heard = listen_and_recognize()
name, dist = find_best_menu(heard, SIDE_MENU) if heard else (None, None)
if not name:
speak("다시 한 번 말씀해주세요")
heard = listen_and_recognize()
name, dist = find_best_menu(heard, SIDE_MENU) if heard else (None, None)
cart["사이드"] = name if name else SIDE_MENU[0]
screen.fill(BG)
screen.blit(small_font.render(f"선택하신 메뉴: {cart['사이드']}", True, DONE_GREEN), (60, 30))
draw_buttons(side_buttons, "사이드")
pygame.display.flip()
pygame.time.delay(1200)
# 3. 음료 주문받기 (버거와 똑같은 순서, 문구만 다름)
screen.fill(BG)
screen.blit(small_font.render("음료를 골라주세요", True, SUB), (60, 30))
draw_buttons(drink_buttons, "음료")
pygame.display.flip()
speak("마지막으로 음료를 골라주세요. 드시고 싶은 음료를 말씀해주세요.")
screen.fill(BG)
screen.blit(small_font.render("듣고 있어요...", True, REC_RED), (60, 30))
draw_buttons(drink_buttons, "음료")
pygame.display.flip()
heard = listen_and_recognize()
name, dist = find_best_menu(heard, DRINK_MENU) if heard else (None, None)
if not name:
speak("다시 한 번 말씀해주세요")
heard = listen_and_recognize()
name, dist = find_best_menu(heard, DRINK_MENU) if heard else (None, None)
cart["음료"] = name if name else DRINK_MENU[0]
screen.fill(BG)
screen.blit(small_font.render(f"선택하신 메뉴: {cart['음료']}", True, DONE_GREEN), (60, 30))
draw_buttons(drink_buttons, "음료")
pygame.display.flip()
pygame.time.delay(1200)
# 4. 다 골랐으니 장바구니를 보여주고 카드 넣으라고 안내
state = "cart"
screen.fill(BG)
draw_cart_screen()
pygame.display.flip()
speak("카드를 넣어주세요")
# ---------- 메인 루프 ----------
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif state == "menu" and event.type == pygame.MOUSEBUTTONDOWN:
for b in burger_buttons:
if b["rect"].collidepoint(event.pos):
cart["버거"] = b["name"]
last_interaction = pygame.time.get_ticks() # 터치했으니 다시 10초 카운트
for b in side_buttons:
if b["rect"].collidepoint(event.pos):
cart["사이드"] = b["name"]
last_interaction = pygame.time.get_ticks()
for b in drink_buttons:
if b["rect"].collidepoint(event.pos):
cart["음료"] = b["name"]
last_interaction = pygame.time.get_ticks()
if cart["버거"] and cart["사이드"] and cart["음료"]: # 세 가지 다 골랐으면
state = "cart" # 자동으로 장바구니 -> 결제 화면
elif state == "cart" and event.type == pygame.MOUSEBUTTONDOWN:
if pay_btn.collidepoint(event.pos):
state = "done"
elif state == "done" and event.type == pygame.MOUSEBUTTONDOWN:
if restart_btn.collidepoint(event.pos):
cart = {"버거": None, "사이드": None, "음료": None}
state = "menu"
last_interaction = pygame.time.get_ticks()
# 10초 동안 터치가 없으면 음성 주문 시작 (menu 화면에서만 확인)
if state == "menu" and pygame.time.get_ticks() - last_interaction >= AUTO_LISTEN_MS:
voice_order()
screen.fill(BG)
if state == "menu":
draw_menu_screen()
elif state == "cart":
draw_cart_screen()
elif state == "done":
draw_done_screen()
pygame.display.flip()
clock.tick(60)
pygame.quit()