🎤 음성 주문 키오스크 — 전체 코드
voice-demo-code.py · pygame + speech_recognition + pyttsx3 (STT/TTS + 레벤슈타인)
실행하려면 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()