UNLV

[UNLV] - 오프라인 음성 인식으로 Tello 드론 제어(11일차)

빡성 2026. 2. 20. 03:18

1. 수업 개요

이번 수업은 오프라인 음성 인식으로 Tello 드론을 실시간 제어하는 내용이다. 인터넷이 없어도 “Take off”, “Land”, “Stop” 같은 말을 인식해 드론을 조종할 수 있도록, PC에서 쓰기 좋은 엔진(Vosk, OpenWakeWord, PocketSphinx)과 마이크 라이브러리(SoundDevice)를 사용했다. 이론에서는 왜 실시간 드론 제어에 Vosk가 적합한지, Whisper와의 차이, 네이티브 오디오(SoundDevice)의 장점을 정리하고, 실습에서는 Vosk 단독 제어, OpenWakeWord(웨이크워드 “Hey Jarvis”) 후 Vosk 명령, PocketSphinx 제어, 그리고 Google Speech Commands 데이터셋·MFCC CNN 학습·TFLite·라즈베리파이·M5Core2 추론까지의 흐름을 다룬다.

2. 실시간 드론 제어와 오프라인 음성 인식

왜 Vosk인가? (Vosk vs Whisper)

음성을 글자로 바꾸는 STT(Speech-to-Text) 중에서 OpenAI의 Whisper는 정확도가 높지만, 실시간 드론 제어에는 Vosk가 더 적합하다.

  • 지연 시간(Latency): Vosk는 오디오를 스트리밍으로 처리해 밀리초 단위 반응이 가능하다. Whisper는 보통 몇 초 단위 청크를 모아 처리하므로 지연이 크다. “Stop”이라고 했을 때 드론이 즉시 멈추려면 Vosk 같은 저지연 엔진이 필요하다.
  • 경량성: Vosk는 일반 CPU만으로 동작하며, 무거운 GPU가 필요 없다. 노트북·RPi에서도 실행하기 쉽다.
  • 설치: pip install vosk로 설치하고, 작은 언어 모델(vosk-model-small-en-us-0.15 등)을 받아 폴더만 두면 된다. Whisper나 다른 엔진은 빌드·의존성 문제가 생기는 경우가 많다.

네이티브 오디오: SoundDevice

Python 음성 인식에서 가장 까다로운 부분은 마이크 라이브러리다. PyAudio는 Windows·Mac에서 Visual Studio Build Tools, Homebrew, PortAudio 등 시스템 의존성을 요구해 설치가 번거롭다. 이 수업에서는 SoundDevice를 쓴다. SoundDevice는 Windows·macOS용 사전 빌드 바이너리(DLL/dylib)를 포함해, 별도 컴파일 없이 pip install sounddevice만으로 마이크 입력을 사용할 수 있다. 따라서 “네이티브” 방식으로 Mac·Windows 모두에서 동일한 코드가 동작하도록 구성했다.

3. 음성 엔진 비교: Vosk, OpenWakeWord, PocketSphinx

Vosk: 오프라인 STT. 스트리밍 처리, 낮은 지연, pip·소형 모델로 설치 간단. 드론 명령어(“take off”, “land”, “stop” 등) 인식에 적합.

OpenWakeWord: “Hey Jarvis” 같은 웨이크 워드만 감지하는 경량 모델. 항상 전체 어휘를 듣지 않고, 웨이크 워드가 나온 뒤에만 Vosk로 명령을 받으면 오인식과 배터리 소모를 줄일 수 있다.

PocketSphinx: 클래식한 오프라인 엔진. 설치 시 SWIG·C++ 빌드 도구가 필요해 Windows·Mac에서 설정이 다소 까다롭다. 짧은 단어(“up” 등)보다 “go up”, “go down”처럼 구문으로 인식시키면 정확도가 올라간다. SoundDevice로 녹음한 뒤 SpeechRecognition으로 PocketSphinx를 호출하는 방식으로 사용할 수 있다.

4. 실습 1: Vosk + Tello (네이티브 오디오)

4-1. 설치 및 모델 다운로드

터미널( Mac) 또는 명령 프롬프트(Windows)에서:

pip install vosk sounddevice djitellopy numpy

Vosk는 소형 언어 모델 폴더가 필요하다. (1) vosk-model-small-en-us-0.15(약 40MB) zip을 다운로드하고, (2) 압축을 푼 뒤 (3) 폴더 이름을 model 또는 vosk_model로 맞추고, (4) 프로젝트 폴더 안에 넣는다. 폴더 안에 am, conf, graph 등이 있어야 한다.

4-2. 제어 흐름

SoundDevice로 마이크에서 16kHz·16bit·모노 오디오를 받아, 큐를 통해 Vosk에 넘긴다. 한 문장이 완성되면 인식 결과를 파싱해 “take off”, “land”, “up”, “down”, “left”, “right”, “forward”, “back”, “stop”, “flip” 등으로 Tello를 제어한다. Tello 연결 실패 시 시뮬레이션 모드로 동작하도록 예외 처리해 두면 편하다.

Vosk + Tello 제어 — 핵심 구조
import sys, json, queue
import sounddevice as sd
from vosk import Model, KaldiRecognizer
from djitellopy import Tello

SPEED = 30
MODEL_PATH = "model"   # 또는 "vosk_model"

tello = Tello()
try:
    tello.connect()
    tello.streamoff()
    print(f"[SUCCESS] Battery: {tello.get_battery()}%")
except Exception as e:
    print(f"[WARN] Tello connection failed: {e}")
    tello = None

q = queue.Queue()
def callback(indata, frames, time, status):
    if status: print(status, file=sys.stderr)
    q.put(bytes(indata))

def parse_and_execute(text):
    if not text: return
    print(f" >> Heard: '{text}'")
    if not tello: return
    try:
        if "take off" in text: tello.takeoff()
        elif "land" in text: tello.land()
        elif "up" in text: tello.move_up(SPEED)
        elif "down" in text: tello.move_down(SPEED)
        elif "left" in text: tello.move_left(SPEED)
        elif "right" in text: tello.move_right(SPEED)
        elif "forward" in text: tello.move_forward(SPEED)
        elif "back" in text: tello.move_back(SPEED)
        elif "stop" in text: tello.send_rc_control(0,0,0,0)
        elif "flip" in text: tello.flip_forward()
    except Exception as e: print(f"Command Error: {e}")

model = Model(MODEL_PATH)
rec = KaldiRecognizer(model, 16000)
with sd.RawInputStream(samplerate=16000, blocksize=8000, dtype='int16', channels=1, callback=callback):
    while True:
        data = q.get()
        if rec.AcceptWaveform(data):
            result = json.loads(rec.Result())
            parse_and_execute(result.get("text", ""))

5. 실습 2: OpenWakeWord + Vosk (웨이크워드 후 명령)

5-1. 구성

(1) OpenWakeWord가 “Hey Jarvis”를 감지하면 (2) 명령 모드로 전환해 (3) Vosk가 “Take off”, “Up” 등의 명령을 인식하고 Tello를 제어한다. SoundDevice로 16kHz PCM을 받아, 웨이크 워드 구간에는 OpenWakeWord에만 넘기고, 명령 모드일 때만 Vosk에 넘긴다.

5-2. 설치 및 모델

pip install sounddevice numpy openwakeword vosk djitellopy

Tello는 Wi‑Fi로 연결되므로, 연결 전에 모델을 미리 받아 두어야 한다. Vosk 모델은 위와 같이 폴더로 준비하고, OpenWakeWord 모델은 라이브러리에서 제공하는 다운로드 함수를 한 번 실행해 받아 둔다.

OpenWakeWord 모델 다운로드
import openwakeword
openwakeword.utils.download_models()
print("OpenWakeWord models downloaded!")

5-3. 실행 흐름

“웨이크 워드 대기”와 “명령 청취”를 플래그로 구분한다. OpenWakeWord 예측값이 임계값(예: 0.5)을 넘으면 명령 모드로 바꾸고 Vosk 버퍼를 리셋한 뒤, 이후 오디오는 Vosk로만 넘긴다. Vosk가 문장을 완성하면 해당 명령으로 드론을 제어하고, 다시 웨이크 워드 모드로 돌아간다.

웨이크 워드 감지 후 Vosk 명령 처리
# 웨이크 워드 대기 중일 때: OpenWakeWord로 "hey_jarvis" 감지
# 명령 모드일 때: Vosk로 "take off", "up" 등 인식 후 실행
if not is_command_mode:
    audio_np = np.frombuffer(data, dtype=np.int16)
    prediction = oww.predict(audio_np)
    if prediction[WAKE_WORD] > 0.5:
        print("[!] WAKE WORD DETECTED! Speak now...")
        is_command_mode = True
        v_rec.Reset()
else:
    if v_rec.AcceptWaveform(data):
        result = json.loads(v_rec.Result())
        cmd_text = result.get("text", "")
        if cmd_text:
            execute_command(cmd_text, drone)
            is_command_mode = False
            oww.reset()

실행 시 PC를 Tello Wi‑Fi(TELLO-XXXXXX)에 연결한 뒤 프로그램을 켠다. “Hey Jarvis”라고 부르고, 이어서 “Take off” 등을 말하면 드론이 반응한다.

6. 실습 3: PocketSphinx + Tello

PocketSphinx는 오프라인 인식이 가능한 클래식 엔진이다. 설치 시 Windows는 Visual C++ Build Tools, Mac은 Homebrew로 swig·portaudio 등을 설치해야 할 수 있다. SoundDevice로 일정 시간(예: 3.5초) 녹음한 뒤 SpeechRecognition 라이브러리로 PocketSphinx를 호출한다. 짧은 단어(“up” 등)보다 “go up”, “go down”, “take off”, “land”처럼 구문으로 매핑하면 인식률이 좋다.

PocketSphinx + SoundDevice — 녹음·인식 루프
DURATION = 3.5   # seconds
SAMPLE_RATE = 16000
r = sr.Recognizer()
while True:
    recording = sd.rec(int(DURATION * SAMPLE_RATE), samplerate=SAMPLE_RATE, channels=1, dtype='int16')
    sd.wait()
    audio_data = sr.AudioData(recording.tobytes(), SAMPLE_RATE, 2)
    try:
        command = r.recognize_sphinx(audio_data)
        execute_command(command)   # "take off", "go up", "land" 등 매핑
    except sr.UnknownValueError:
        pass

7. 실습 4: 데이터셋·CNN 학습·임베디드 추론

7-1. 데이터셋 준비

드론 명령어를 키워드/클래스로 구분해 학습하려면, 클래스별 폴더에 1초 길이 WAV(16kHz 또는 8kHz, 모노)를 넣는다. 예: takeoff, land, up, down, left, right, 배경 소음용 폴더 등. 직접 녹음하거나, Google Speech Commands v0.02를 받아 필요한 단어(on, up, down, left, right, off, 배경 소음 등)만 추출해 쓸 수 있다. UAV 명령용 데이터셋은 Command Dataset 링크에서 받을 수 있다. “Tello”처럼 공개 데이터셋에 없는 단어는 gTTS·pydub로 1초 WAV를 만들어 넣을 수 있다.

7-2. MFCC + 2D CNN 학습

librosa로 오디오에서 MFCC를 뽑고, 2D CNN(Conv2D + MaxPooling + Dense)으로 명령어를 분류한다. 샘플레이트(16kHz), 1초 길이, MFCC 개수(13) 등을 맞춰 두고, 클래스별 폴더를 돌며 로드한다. 배경 소음은 긴 파일을 1초 단위로 잘라 여러 샘플로 쓴다. 학습 후 모델을 저장하고, 정확도·F1·혼동 행렬·분류 리포트로 평가한다. 임베디드용으로 쓸 때는 8kHz·1초 raw 또는 MFCC로 입력을 맞춘 뒤 TFLite로 Full Integer 양자화해 내보내면 된다.

7-3. TFLite·라즈베리파이·M5Core2 요약

전체 흐름은 다음과 같다.

(1) Python: 8kHz·1초 raw 오디오를 입력으로 하는 1D CNN을 학습하고, TFLite Full Integer 양자화로 모델을 내보낸다.

(2) M5Core2(ESP32): 내보낸 모델을 C 배열로 변환해 TFLite Micro로 올리면, 보드 마이크로 1초 녹음 후 추론할 수 있다.

(3) 라즈베리파이 Zero 2 W: TFLite 런타임과 sounddevice로 8kHz·1초 캡처 후, 모델이 Int8 입력이면 Float32를 Int8로 바꿔 추론하고, 인식된 라벨에 따라 Tello 제어를 붙일 수 있다. 라즈베리파이에서 마이크가 안 잡히면 사용 중인 오디오 장치 번호를 확인한 뒤, 해당 장치를 지정해 녹음하면 된다.

8. 정리

이번 수업에서는 실시간 드론 제어에 적합한 오프라인 음성 인식을 다뤘다. Vosk는 지연이 적고 경량이라 “Stop” 같은 즉각 반응이 필요할 때 Whisper보다 유리하다. 마이크 입력은 PyAudio 대신 SoundDevice를 쓰면 Mac·Windows에서 같은 방식으로 동작한다. 실습 1에서는 Vosk만으로 Tello를 제어하고, 실습 2에서는 OpenWakeWord(“Hey Jarvis”)를 먼저 듣고 나서 Vosk로 명령을 받는 방식, 실습 3에서는 PocketSphinx와 SoundDevice로 고정 길이 녹음 후 인식하는 방식을 사용했다. 실습 4에서는 Google Speech Commands에서 필요한 단어만 추출·준비하고, Tello 등 커스텀 단어는 음성 합성으로 보강한 뒤, MFCC와 2D CNN으로 학습하고, TFLite로 내보내서 라즈베리파이·M5Core2 같은 보드에서 추론하는 흐름까지 정리했다.