🎤 음성 주문 키오스크 — 챕터별로 직접 만들기
각 챕터는 이전 챕터의 전체 코드를 그대로 포함하고, 새 기능만 이어서 더합니다. 9번까지 순서대로 따라가면 실제 완성 데모와 같은 프로그램이 됩니다.
목차 — 완료하면 체크해보세요
  1. 1. 메뉴 화면 만들기
  2. 2. 터치로 메뉴 고르기
  3. 3. 장바구니 → 결제까지 완성하기
  4. 4. 마이크로 듣고 텍스트로 바꾸기
  5. 5. 주변 소음 줄이기
  6. 6. 레벤슈타인 거리로 메뉴 이름 맞추기
  7. 7. 글자를 소리로 읽어주기 (TTS)
  8. 8. 음성으로 버거 하나 주문받기
  9. 9. 마무리 — 사이드·음료까지 잇고 자동 시작
준비물: pip install pygame SpeechRecognition pyaudio pyttsx3. pygame.Rect 선언/그리기와 마우스 클릭 이벤트는 이미 할 수 있다고 가정합니다. 각 챕터의 코드는 바로 이전 챕터의 전체 코드 + 이번 챕터에서 새로 추가되는 부분입니다.  파란색으로 밑칠된 줄 이 이번 챕터에서 새로 생긴 부분입니다. 챕터 4~7은 아직 전체 앱에 합치기 전에, 그 기능 하나만 따로 테스트해보는 작은 프로그램입니다.

1메뉴 화면 만들기

버거(2종) · 사이드(3종) · 음료(3종), 세 줄로 메뉴 버튼을 그립니다. seat-demo의 책상처럼, 메뉴도 이름 + 위치를 담은 딕셔너리를 리스트에 모아서 관리합니다.

import pygame

pygame.init()
screen = pygame.display.set_mode((1000, 650))          # 키오스크 화면 크기
font = pygame.font.SysFont("malgungothic", 26)
clock = pygame.time.Clock()

BG, INK, CARD, LINE = (253, 248, 240), (26, 16, 8), (255, 255, 255), (176, 96, 16)

BURGER_MENU = ["치킨버거", "불고기버거"]
SIDE_MENU = ["감자튀김", "어니언링", "치즈스틱"]
DRINK_MENU = ["콜라", "사이다", "환타"]

# 버거 버튼들을 한 줄로 만든다 (100+220칸씩 옆으로)
burger_buttons = []
for i, name in enumerate(BURGER_MENU):
    burger_buttons.append({"name": name, "rect": pygame.Rect(100 + i * 220, 140, 200, 100)})

# 사이드도 똑같은 방식으로, y좌표만 다르게
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)})

def draw_buttons(button_list):
    for b in button_list:
        pygame.draw.rect(screen, CARD, 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))

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    screen.fill(BG)
    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)

    pygame.display.flip()
    clock.tick(60)

pygame.quit()

버거/사이드/음료를 하나로 합친 함수 대신, 거의 똑같은 코드를 세 번 반복했습니다. 나중에 각 줄마다 하는 일이 조금씩 달라질 수 있어서, 처음부터 억지로 합치기보다 이렇게 풀어두는 게 이해하기 쉽습니다.

직접 해보기 사이드 메뉴에 "치즈볼"을 하나 추가해보세요. 버튼 4개가 화면 밖으로 나가지 않으려면 어떻게 해야 할까요?

2터치로 메뉴 고르기

클릭한 메뉴를 cart 딕셔너리에 저장하고, 고른 메뉴는 버튼 색을 노란색으로 바꿔서 보여줍니다.

import pygame

pygame.init()
screen = pygame.display.set_mode((1000, 650))
font = pygame.font.SysFont("malgungothic", 26)
clock = pygame.time.Clock()

BG, INK, CARD, LINE = (253, 248, 240), (26, 16, 8), (255, 255, 255), (176, 96, 16)
SELECTED = (255, 224, 150)   # 선택된 메뉴 버튼 색

BURGER_MENU = ["치킨버거", "불고기버거"]
SIDE_MENU = ["감자튀김", "어니언링", "치즈스틱"]
DRINK_MENU = ["콜라", "사이다", "환타"]

cart = {"버거": None, "사이드": None, "음료": None}   # 아직 아무것도 안 고르면 전부 None

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)})

# cart_key: 이 버튼 줄이 "버거"인지 "사이드"인지 "음료"인지
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))

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

        elif event.type == pygame.MOUSEBUTTONDOWN:
            for b in burger_buttons:
                if b["rect"].collidepoint(event.pos):
                    cart["버거"] = b["name"]
            for b in side_buttons:
                if b["rect"].collidepoint(event.pos):
                    cart["사이드"] = b["name"]
            for b in drink_buttons:
                if b["rect"].collidepoint(event.pos):
                    cart["음료"] = b["name"]

    screen.fill(BG)
    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, "음료")

    pygame.display.flip()
    clock.tick(60)

pygame.quit()
직접 해보기 버거·사이드·음료를 각각 눌러보고, 다른 걸 다시 누르면 선택이 바뀌는지(같은 줄에서 하나만 선택되는지) 확인하세요.

3장바구니 → 결제까지 완성하기

세 가지를 다 고르면 자동으로 장바구니 화면이 뜨고, "카드를 넣어주세요" 버튼을 누르면 완료 화면으로 넘어갑니다. 화면이 여러 개가 됐으니 지금이 어느 화면인지 기억해둘 state 변수가 필요합니다.

import pygame

pygame.init()
screen = pygame.display.set_mode((1000, 650))
font = pygame.font.SysFont("malgungothic", 26)
big_font = pygame.font.SysFont("malgungothic", 34)   # 화면 제목용 큰 글꼴
clock = pygame.time.Clock()

BG, INK, CARD, LINE = (253, 248, 240), (26, 16, 8), (255, 255, 255), (176, 96, 16)
SELECTED = (255, 224, 150)
DONE_GREEN = (10, 122, 48)

BURGER_MENU = ["치킨버거", "불고기버거"]
SIDE_MENU = ["감자튀김", "어니언링", "치즈스틱"]
DRINK_MENU = ["콜라", "사이다", "환타"]

cart = {"버거": None, "사이드": None, "음료": None}
state = "menu"   # "menu"(고르기) -> "cart"(장바구니) -> "done"(완료)

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(1000 // 2 - 110, 650 - 100, 220, 60)      # 장바구니 화면의 버튼
restart_btn = pygame.Rect(1000 // 2 - 110, 650 - 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, "음료")

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=(1000 // 2, 650 // 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))

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"]
            for b in side_buttons:
                if b["rect"].collidepoint(event.pos):
                    cart["사이드"] = b["name"]
            for b in drink_buttons:
                if b["rect"].collidepoint(event.pos):
                    cart["음료"] = b["name"]
            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"

    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()

여기까지가 터치로만 주문하는 키오스크입니다. 이 상태로도 완전히 동작합니다 — 다음 챕터부터는 "시니어 어르신을 위한 음성 안내"를 추가해나갑니다.

직접 해보기 메뉴를 다 고르고 → 카드 넣기 → 처음으로 버튼까지 눌러서, 전체 흐름이 한 바퀴 도는지 확인하세요.

4마이크로 듣고 텍스트로 바꾸기

여기서부터 4개 챕터(4~7)는 잠깐 키오스크 화면에서 벗어나, 음성 기능 하나씩만 따로 테스트해봅니다. 다 만들어지면 8번 챕터에서 지금까지 만든 키오스크에 합칩니다.

import pygame
import speech_recognition as sr

pygame.init()
screen = pygame.display.set_mode((1000, 650))
font = pygame.font.SysFont("malgungothic", 26)
small_font = pygame.font.SysFont("malgungothic", 20)
clock = pygame.time.Clock()

BG, INK, LINE = (253, 248, 240), (26, 16, 8), (176, 96, 16)

mic_btn = pygame.Rect(400, 300, 200, 70)
recognizer = sr.Recognizer()
status_text = "마이크 버튼을 눌러 말해보세요"
recognized_text = ""

# 마이크로 소리를 듣고, 텍스트로 바꿔서 recognized_text에 저장한다
def listen_and_recognize():
    global status_text, recognized_text
    status_text = "듣고 있어요..."
    pygame.display.flip()

    with sr.Microphone() as source:
        try:
            audio = recognizer.listen(source, timeout=5, phrase_time_limit=6)
        except sr.WaitTimeoutError:
            status_text = "음성이 감지되지 않았어요"
            return

    try:
        recognized_text = recognizer.recognize_google(audio, language="ko-KR")
        status_text = "인식 완료!"
    except sr.UnknownValueError:
        status_text = "못 알아들었어요, 다시 시도해주세요"
    except sr.RequestError:
        status_text = "네트워크 오류"

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.MOUSEBUTTONDOWN:
            if mic_btn.collidepoint(event.pos):
                listen_and_recognize()

    screen.fill(BG)
    pygame.draw.rect(screen, LINE, mic_btn, border_radius=10)
    mic_label = font.render("눌러서 말하기", True, (255, 255, 255))
    screen.blit(mic_label, mic_label.get_rect(center=mic_btn.center))

    screen.blit(small_font.render(status_text, True, INK), (60, 100))
    screen.blit(small_font.render(f"인식된 문장: {recognized_text}", True, INK), (60, 140))

    pygame.display.flip()
    clock.tick(60)

pygame.quit()

timeout=5는 "5초 안에 말을 시작하지 않으면 포기", phrase_time_limit=6은 "한 문장은 최대 6초까지만 듣는다"는 뜻입니다.

직접 해보기 버튼을 누르고 "치킨버거 주세요"라고 말해보세요. recognized_text에 그대로 찍히는지 확인하세요.

5주변 소음 줄이기

키오스크는 매장 소음(음악, 다른 손님 대화) 속에 놓입니다. adjust_for_ambient_noise()가 말하기 직전 잠깐 주변 소음을 측정해서 "이 정도부터는 말소리"라는 기준을 자동으로 맞춰줍니다.

import pygame
import speech_recognition as sr

pygame.init()
screen = pygame.display.set_mode((1000, 650))
font = pygame.font.SysFont("malgungothic", 26)
small_font = pygame.font.SysFont("malgungothic", 20)
clock = pygame.time.Clock()

BG, INK, LINE = (253, 248, 240), (26, 16, 8), (176, 96, 16)

mic_btn = pygame.Rect(400, 300, 200, 70)
recognizer = sr.Recognizer()
status_text = "마이크 버튼을 눌러 말해보세요"
recognized_text = ""

def listen_and_recognize():
    global status_text, recognized_text
    status_text = "듣고 있어요..."
    pygame.display.flip()

    with sr.Microphone() as source:
        status_text = "주변 소리를 측정하는 중..."
        pygame.display.flip()
        recognizer.adjust_for_ambient_noise(source, duration=0.6)   # 0.6초 동안 소음 측정
        status_text = "듣고 있어요..."
        pygame.display.flip()
        try:
            audio = recognizer.listen(source, timeout=5, phrase_time_limit=6)
        except sr.WaitTimeoutError:
            status_text = "음성이 감지되지 않았어요"
            return

    try:
        recognized_text = recognizer.recognize_google(audio, language="ko-KR")
        status_text = "인식 완료!"
    except sr.UnknownValueError:
        status_text = "못 알아들었어요, 다시 시도해주세요"
    except sr.RequestError:
        status_text = "네트워크 오류"

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.MOUSEBUTTONDOWN:
            if mic_btn.collidepoint(event.pos):
                listen_and_recognize()

    screen.fill(BG)
    pygame.draw.rect(screen, LINE, mic_btn, border_radius=10)
    mic_label = font.render("눌러서 말하기", True, (255, 255, 255))
    screen.blit(mic_label, mic_label.get_rect(center=mic_btn.center))

    screen.blit(small_font.render(status_text, True, INK), (60, 100))
    screen.blit(small_font.render(f"인식된 문장: {recognized_text}", True, INK), (60, 140))

    pygame.display.flip()
    clock.tick(60)

pygame.quit()
duration을 너무 길게 잡으면(예: 3초) 어르신이 말을 걸기도 전에 시간이 다 지나가버릴 수 있습니다. 0.5~1초가 적당합니다.

6레벤슈타인 거리로 메뉴 이름 맞추기

STT가 "치킨버거"를 "치킨버그"로 잘못 알아듣는 경우가 많습니다. seat-demo에서도 쓴 레벤슈타인 거리(편집 거리)로 가장 비슷한 메뉴를 찾습니다. 이번엔 화면도 필요 없이, 로직만 테스트합니다.

# 두 문자열이 같아지려면 몇 번 고쳐야 하는지 계산 (삽입/삭제/변경 각 1점)
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]

# text와 가장 비슷한 메뉴 이름과, 그때의 편집 거리를 돌려준다
def find_best_menu(text, menu_list):
    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

BURGER_MENU = ["치킨버거", "불고기버거"]

# STT가 "치킨버그"로 잘못 알아들었다고 가정하고 테스트
name, dist = find_best_menu("치킨버그", BURGER_MENU)
print(name, dist)   # -> 치킨버거 1

이 챕터는 pygamespeech_recognition도 필요 없습니다 — 순수하게 문자열 비교 로직이기 때문입니다. 그냥 파이썬 파일로 저장해서 실행해도 됩니다.

직접 해보기 find_best_menu("불고기버거요", BURGER_MENU)find_best_menu("콜라", BURGER_MENU)를 각각 실행해보세요. 메뉴에 아예 없는 말을 넣으면 거리가 얼마나 크게 나오나요?

7글자를 소리로 읽어주기 (TTS)

pyttsx3로 글자를 소리 내어 읽게 만듭니다. STT(듣기)의 반대인 TTS(말하기)입니다.

import pyttsx3

def speak(text):
    engine = pyttsx3.init()   # 매번 새로 만든다 (아래 설명 참고)
    engine.say(text)
    engine.runAndWait()
    engine.stop()

speak("안녕하세요. 주문을 시작하겠습니다.")
speak("치킨버거를 골라주세요.")
pyttsx3는 엔진 하나를 계속 재사용해서 runAndWait()을 여러 번 부르면, 두 번째 호출부터 소리가 안 나는 경우가 있습니다(특히 윈도우에서). 그래서 speak()를 부를 때마다 pyttsx3.init()으로 엔진을 새로 만듭니다.
직접 해보기 speak()를 5번 연달아 호출해서, 전부 소리가 잘 나오는지 확인하세요. (엔진을 한 번만 만들어서 재사용하도록 바꿔보고, 두 번째 호출부터 소리가 안 나는지도 비교해보면 왜 이 방식을 쓰는지 확실히 알 수 있습니다.)

8음성으로 버거 하나 주문받기

3번 챕터(완성된 키오스크)에 4~7번에서 따로 테스트한 기능(STT·소음 줄이기·레벤슈타인·TTS)을 전부 합쳐서, "버거" 한 카테고리만 음성으로 주문받아 봅니다. 메뉴 화면에 임시 버튼을 하나 추가해서, 누르면 음성 주문이 시작되도록 만듭니다.

import pygame
import speech_recognition as sr
import pyttsx3

pygame.init()
screen = pygame.display.set_mode((1000, 650))
font = pygame.font.SysFont("malgungothic", 26)
small_font = pygame.font.SysFont("malgungothic", 18)
big_font = pygame.font.SysFont("malgungothic", 34)
clock = pygame.time.Clock()

BG, INK, CARD, LINE = (253, 248, 240), (26, 16, 8), (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 = ["콜라", "사이다", "환타"]

cart = {"버거": None, "사이드": None, "음료": None}
state = "menu"

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(1000 // 2 - 110, 650 - 100, 220, 60)
restart_btn = pygame.Rect(1000 // 2 - 110, 650 - 100, 220, 60)
voice_burger_btn = pygame.Rect(60, 560, 280, 50)   # 테스트용 임시 버튼 (9번 챕터에서 없어짐)

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, "음료")
    pygame.draw.rect(screen, LINE, voice_burger_btn, border_radius=10)
    vlabel = small_font.render("🎤 음성으로 버거 주문 (테스트용)", True, (255, 255, 255))
    screen.blit(vlabel, vlabel.get_rect(center=voice_burger_btn.center))

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=(1000 // 2, 650 // 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))

# ---- 5·6·7번 챕터에서 테스트한 함수들을 그대로 가져옴 ----
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 ""

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):
    best_name, best_dist = None, 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

def speak(text):
    engine = pyttsx3.init()
    engine.say(text)
    engine.runAndWait()
    engine.stop()

# ---- 버거 하나를 음성으로 주문받는 함수 ----
def order_burger_by_voice():
    global cart

    screen.fill(BG)
    screen.blit(small_font.render("버거를 골라주세요", True, INK), (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)

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:
            if voice_burger_btn.collidepoint(event.pos):
                order_burger_by_voice()
            for b in burger_buttons:
                if b["rect"].collidepoint(event.pos):
                    cart["버거"] = b["name"]
            for b in side_buttons:
                if b["rect"].collidepoint(event.pos):
                    cart["사이드"] = b["name"]
            for b in drink_buttons:
                if b["rect"].collidepoint(event.pos):
                    cart["음료"] = b["name"]
            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"

    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()

버튼을 눌러서 직접 테스트할 수 있으니, 9번 챕터로 넘어가기 전에 이 버튼으로 "버거" 음성 주문이 잘 되는지 충분히 확인해보세요.

직접 해보기 일부러 못 알아들을 만한 말을 해보고, "다시 한 번 말씀해주세요"가 나온 뒤 그래도 실패하면 BURGER_MENU[0](치킨버거)로 자동 선택되는지 확인하세요.

9마무리 — 사이드·음료까지 잇고 자동 시작

8번 챕터의 order_burger_by_voice()를 사이드·음료용으로 두 번 더 복사해서 이어 붙이고, 임시 버튼 대신 10초 유휴 타이머로 자동 시작하도록 바꾸면 완성입니다. 새로운 개념은 없고, 지금까지 배운 것을 그대로 이어 붙이는 마지막 단계입니다.

바뀌는 것 3가지

  • order_burger_by_voice() 뒤에 사이드용, 음료용을 문구만 바꿔서 그대로 이어 붙입니다 (안내 멘트: "먼저 버거를 골라주세요" → "이번엔 사이드를 골라주세요" → "마지막으로 음료를 골라주세요").
  • 세 단계가 다 끝나면 state = "cart"로 바꾸고 "카드를 넣어주세요"를 speak()합니다.
  • voice_burger_btn 버튼을 지우고, 대신 last_interaction(마지막으로 화면을 건드린 시각)을 기록해뒀다가 10초가 지나면 자동으로 이 함수를 실행합니다 (voice-guide 이전 6번 챕터에서 쓴 AUTO_LISTEN_MS 방식과 동일).

전체 코드는 이 사이트의 완성된 데모 → "코드 보기" 버튼에서 그대로 확인할 수 있습니다. 함수 이름과 구조가 지금까지와 완전히 똑같아서, order_burger_by_voice() 옆에 order_side_by_voice(), order_drink_by_voice()가 나란히 있는 걸 바로 알아볼 수 있을 겁니다.

여기까지 오셨다면

메뉴 화면 → 터치 주문 → 장바구니/결제 → 음성 인식 → 소음 보정 → 레벤슈타인 매칭 → TTS 안내 → 한 카테고리 음성 주문 → 전체 3단계 자동 음성 주문까지, 시니어 음성 키오스크의 모든 기능을 직접 만든 것입니다.