AizuDemy

Tutorial Build Real-Time Voice AI Agent Lokal: Whisper, Ollama & Piper TTS

Tutorial Build Real-Time Voice AI Agent Lokal: Whisper, Ollama & Piper TTS
๐ŸŽง
Dengarkan Artikel Ini
Suara AI Otomatis โ€ข 9 mnt baca baca
โšก TL;DR

Poin Kunci Artikel Ini:

  • Gunakan ukuran frame fixed 512 sampel (32ms pada 16kHz).
  • Evaluasi probabilitas suara manusia per frame (ambang batas p > 0.5).
  • Potong aliran audio setelah 500ms hening terkonfirmasi. Penghematan durasi perekaman 700ms ini langsung mengurangi durasi beban kerja transkripsi Whisper.cpp.
๐Ÿ“‹ Daftar Isi Materi Tutup โ–ด

Tantangan Latensi Voice AI dan Privasi Data Lokal

Arsitektur Voice AI berbasis cloud seperti OpenAI Realtime API atau ElevenLabs memiliki tiga batasan utama: latensi jaringan (Network RTT), biaya operasional variabel per menit, dan kerentanan privasi data audio. Setiap klip suara pengguna harus melalui serialisasi audio, enkripsi TLS, transmisi jaringan publik, inferensi server cloud, dan deserialisasi balik ke klien. Proses ini menghasilkan latensi end-to-end berkisar antara 1.500 ms hingga 3.500 ms.

Privasi data audio juga menjadi kendala pada sektor teregulasi seperti perbankan, kesehatan, dan infrastruktur kritis. Mengirim rekaman suara mentah ke endpoint server pihak ketiga melanggar regulasi privasi data lokal seperti PDP di Indonesia atau GDPR di Eropa.

Solusi teknis: Jalankan seluruh pipeline Speech-to-Text (STT), Large Language Model (LLM), dan Text-to-Speech (TTS) secara lokal di edge device atau hardware internal. Pipeline lokal menghilangkan dependensi jaringan luar, memotong biaya API menjadi nol, dan menjaga data audio berada di dalam memori lokal.

Komponen Utama dan Arsitektur Pipeline Lokal

Sistem Voice AI Agent lokal latensi rendah ini mengombinasikan tiga engine open-source yang dioptimasi untuk arsitektur hardware konsumer:

  • Speech-to-Text (STT) - Whisper.cpp: Porting C/C++ dari OpenAI Whisper. Memanfaatkan instruksi CPU SIMD (AVX2, AVX-512, ARM Neon) serta akselerasi GPU (CUDA/Metal). Ukuran memori kecil dalam format GGML/GGUF dengan latensi transkripsi di bawah 200 ms untuk audio pendek.
  • Large Language Model (LLM) - Ollama Engine: Framework eksekusi LLM lokal berbasis llama.cpp. Menggunakan teknik kuantisasi 4-bit (q4_k_m atau q4_0) untuk menjalankan model seperti Llama 3.2 1B atau Mistral 7B pada VRAM/RAM terbatas. Supporting streaming token melalui endpoint HTTP API.
  • Text-to-Speech (TTS) - Piper TTS: Engine sintesis suara neural berbasis arsitektur VITS yang dieksekusi via ONNX Runtime. Mampu menghasilkan audio raw PCM 22.05kHz dengan Real-Time Factor (RTF) di bawah 0.1 pada CPU standar.

Skema aliran data audio dan token antar komponen:

[Microphone Input]
  -> (Audio Stream & VAD Chunking)
  -> [Whisper.cpp STT Engine]
  -> (Text Prompt)
  -> [Ollama LLM Engine]
  -> (Token Stream & Sentence Buffer Splitter)
  -> [Piper TTS Engine]
  -> [Speaker Output PCM Audio]

Target performa total: Latensi percakapan end-to-end (dihitung dari akhir ucapan pengguna hingga audio respons keluar dari speaker) di bawah 800 milidetik pada CPU laptop standar tanpa akselerasi cloud.

Instalasi Dependensi Sistem dan Python Environment

Konfigurasi lingkungan pengembangan membutuhkan dependensi C-library untuk penanganan audio i/o hardware dan pemrosesan stream media.

Langkah 1: Instalasi Paket Sistem Operasi

Jalankan perintah instalasi paket sistem pada terminal sesuai sistem operasi yang digunakan:

# Linux (Ubuntu/Debian)
sudo apt update && sudo apt install -y portaudio19-dev ffmpeg build-essential wget

# macOS (Homebrew)
brew install portaudio ffmpeg wget

# Windows (via WSL2 Ubuntu)
sudo apt update && sudo apt install -y portaudio19-dev ffmpeg build-essential wget

Langkah 2: Setup Python Virtual Environment dan Dependensi

Buat lingkungan terisolasi Python 3.10+ untuk menghindari bentrok paket sistem:

python3 -m venv venv
source venv/bin/activate

# Install Python libraries
pip install sounddevice numpy requests pywhispercpp piper-tts wave

Download Weight Model Whisper, Ollama, dan Piper TTS

Unduh file model kuantisasi yang paling efisien untuk menyeimbangkan akurasi dan kecepatan inferensi hardware.

Langkah 1: Download Model Whisper.cpp (GGML Format)

Gunakan model ggml-base.en.bin (ukuran ~140MB) untuk kasus penggunaan Bahasa Inggris, atau ggml-base.bin untuk dukungan multi-bahasa:

wget https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin -O model-base.bin

Langkah 2: Pull Model LLM Terkuantisasi di Ollama

Pastikan layanan daemon Ollama telah aktif, kemudian unduh model Llama 3.2 1B yang sudah dioptimasi untuk kecepatan respons tinggi:

ollama pull llama3.2:1b

Langkah 3: Unduh Model Suara Piper TTS (ONNX)

Unduh file bobot model neural TTS beserta file konfigurasi JSON pendukungnya:

# Model ONNX Voice en_US-lessac-medium
wget https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx
wget https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx.json

Implementasi Kode Program Utama (voice_agent.py)

Berikut script Python lengkap yang menghubungkan modul audio capture input, transkripsi Whisper.cpp, generasi token LLM Ollama, dan sintesis audio Piper TTS dalam satu pipeline terintegrasi:

import sounddevice as sd
import numpy as np
import requests
import subprocess
import wave
import io
import os
import sys
from pywhispercpp.model import Model

# Configuration Parameters
SAMPLE_RATE = 16000
SILENCE_THRESHOLD = 0.01
SILENCE_DURATION = 1.2 # seconds threshold for speech end
WHISPER_MODEL_PATH = "model-base.bin"
PIPER_MODEL_PATH = "en_US-lessac-medium.onnx"
OLLAMA_URL = "http://localhost:11434/api/generate"

print("Loading Whisper Model into memory...")
whisper_model = Model(WHISPER_MODEL_PATH, n_threads=4)

def record_audio_until_silence():
    print("
[Listening... Speak now]")
    audio_data = []
    silence_counter = 0
    recording = False
    
    def callback(indata, frames, time, status):
        nonlocal silence_counter, recording
        amplitude = np.max(np.abs(indata))
        if amplitude > SILENCE_THRESHOLD:
            recording = True
            silence_counter = 0
            audio_data.append(indata.copy())
        elif recording:
            silence_counter += frames / SAMPLE_RATE
            audio_data.append(indata.copy())

    with sd.InputStream(samplerate=SAMPLE_RATE, channels=1, callback=callback):
        while True:
            sd.sleep(100)
            if recording and silence_counter >= SILENCE_DURATION:
                break
                
    audio_np = np.concatenate(audio_data, axis=0).flatten()
    return audio_np

def transcribe_audio(audio_np):
    temp_wav = "input.wav"
    audio_int16 = (audio_np * 32767).astype(np.int16)
    with wave.open(temp_wav, "wb") as wf:
        wf.setnchannels(1)
        wf.setsampwidth(2)
        wf.setframerate(SAMPLE_RATE)
        wf.writeframes(audio_int16.tobytes())
        
    segments = whisper_model.transcribe(temp_wav)
    text = "".join([segment.text for segment in segments]).strip()
    if os.path.exists(temp_wav):
        os.remove(temp_wav)
    return text

def stream_ollama_llm(prompt):
    payload = {
        "model": "llama3.2:1b",
        "prompt": f"Respond in 1-2 short sentences. User: {prompt}",
        "stream": True
    }
    response = requests.post(OLLAMA_URL, json=payload, stream=True)
    sentence_buffer = ""
    
    for line in response.iter_lines():
        if line:
            data = requests.compat.json.loads(line)
            token = data.get("response", "")
            sentence_buffer += token
            if token in [".", "!", "?", "
"]:
                yield sentence_buffer.strip()
                sentence_buffer = ""
    if sentence_buffer.strip():
        yield sentence_buffer.strip()

def speak_piper(text):
    if not text:
        return
    print(f"Agent: {text}")
    cmd = [
        "piper",
        "--model", PIPER_MODEL_PATH,
        "--output-raw"
    ]
    process = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
    raw_audio, _ = process.communicate(input=text.encode('utf-8'))
    
    audio_np = np.frombuffer(raw_audio, dtype=np.int16).astype(np.float32) / 32768.0
    sd.play(audio_np, samplerate=22050)
    sd.wait()

def main():
    print("Voice Agent Active. Press Ctrl+C to exit.")
    try:
        while True:
            audio_np = record_audio_until_silence()
            user_text = transcribe_audio(audio_np)
            if not user_text:
                continue
            print(f"You: {user_text}")
            
            for chunk in stream_ollama_llm(user_text):
                speak_piper(chunk)
    except KeyboardInterrupt:
        print("
Stopping agent...")

if __name__ == "__main__":
    main()

Bedah Arsitektur dan Alur Kerja Kode

Setiap blok fungsi dalam voice_agent.py menangani tahap pipeline independen secara sekuensial dan terstruktur:

  • Perekaman Audio Tersegmentasi (record_audio_until_silence): Callback sounddevice membaca sinyal audio mentah pada frekuensi 16kHz mono. Sistem membandingkan amplitudo dengan SILENCE_THRESHOLD (0.01). Jika amplitudo berada di bawah ambang batas selama 1.2 detik (SILENCE_DURATION), siklus perekaman berhenti otomatis dan mengembalikan array NumPy.
  • Transkripsi Sinyal Audio (transcribe_audio): Array NumPy dikonversi menjadi format integer 16-bit PCM, lalu ditulis ke file temporal WAV. C++ binding pywhispercpp memproses file WAV menggunakan thread internal untuk menghasilkan string teks ucapan pengguna. File WAV dihapus sesaat setelah eksekusi selesai untuk menghemat memori IO.
  • Streaming LLM dan Parsing Kalimat (stream_ollama_llm): Teks pengguna dikirim ke Ollama HTTP API dengan parameter stream: true. Generator Python menangkap token yang mengalir secara bertahap dan memasukkannya ke dalam sentence_buffer. Begitu pembatas kalimat (titik, tanda seru, atau tanda tanya) terdeteksi, generator langsung meng-yield satu kalimat utuh tanpa menunggu seluruh respons selesai dibuat.
  • Sintesis dan Playback Suara (speak_piper): Teks kalimat yang diterima dari generator dikirim ke stdin proses sub-shell Piper TTS via pipa biner raw. Output audio PCM mentah dibaca ke dalam memori, diubah menjadi normalisasi float32 (-1.0 hingga 1.0), kemudian dimainkan ke output hardware speaker melalui sounddevice.play().

Teknik Optimasi Latensi dan Manajemen Memori

Menjalankan tiga model AI terpisah (STT, LLM, TTS) pada hardware lokal berisiko memicu kemacetan sumber daya CPU/GPU dan peningkatan latensi. Terapkan strategi optimasi teknis berikut untuk menjaga latensi di bawah 800 ms:

1. Sentence-Level Streaming (Overlap Execution)

Pendekatan naif menunggu LLM menyelesaikan seluruh respons paragraf sebelum memanggil TTS akan memicu latensi awal 2-4 detik. Teknik Sentence-Level Streaming membagi respons menjadi blok kalimat independen.

Piper TTS mulai mengeksekusi sintesis audio kalimat pertama sementara LLM Ollama masih melakukan inferensi token untuk kalimat kedua di latar belakang. Overlap eksekusi ini memangkas Time-To-First-Audio (TTFA) hingga 60%.

2. Integrasi Neural Voice Activity Detection (Silero VAD)

Deteksi hening berbasis threshold amplitudo sederhana rentan terhadap noise latar belakang (seperti suara kipas angin atau derak keyboard), yang menyebabkan siklus perekaman menggantung.

Gantikan logika threshold dengan model Silero VAD berbasis ONNX Runtime:

  • Gunakan ukuran frame fixed 512 sampel (32ms pada 16kHz).
  • Evaluasi probabilitas suara manusia per frame (ambang batas p > 0.5).
  • Potong aliran audio setelah 500ms hening terkonfirmasi. Penghematan durasi perekaman 700ms ini langsung mengurangi durasi beban kerja transkripsi Whisper.cpp.

3. Manajemen VRAM dan Alokasi Model Quantization

Untuk mencegah kondisi Out-Of-Memory (OOM) dan penggalan memori swap pada RAM sistem, patuhi matriks alokasi sumber daya berikut:

  • Whisper.cpp: Pilih model ggml-base.en.bin (~140MB RAM) atau ggml-tiny.en.bin (~75MB RAM). Model large-v3 membutuhkan >4GB RAM dan meningkatkan latensi transkripsi hingga 3x lipat pada CPU.
  • Ollama LLM: Batasi ukuran model maksimal 3B parameter dengan kuantisasi 4-bit (misalnya llama3.2:1b atau phi3:mini). Ukuran VRAM yang dibutuhkan berkisar antara 1,2 GB hingga 2,2 GB.
  • Piper TTS: Model varian medium ONNX membutuhkan konsumsi RAM <80MB dengan waktu sintesis <40ms per kalimat.

Panduan Migrasi ke Production Environment

Untuk mengubah script voice_agent.py dasar menjadi arsitektur tingkat produksi (production-ready) yang scalable, ikuti tahapan arsitektural berikut:

  1. Refactoring ke Asynchronous Pipeline (asyncio + Janus Queue): Ganti panggilan synchronous subprocess dan requests dengan aiohttp dan asyncio.subprocess. Gunakan antrean berprioritas (priority queue) untuk memisahkan thread perekaman audio, inferensi LLM, dan pembacaan playback audio tanpa memblokir event loop utama.
  2. Pembungkusan WebSocket Server (FastAPI): Bungkus pipeline Python ke dalam endpoint WebSocket (misal: /ws/voice). Hal ini memungkinkan aplikasi web frontend (React/Vue) atau aplikasi mobile (Flutter/Android) mengirimkan audio stream format WebM/Opus via WebSocket dan menerima return stream audio PCM secara duplex real-time.
  3. Containerization Menggunakan Docker dan CUDA Passthrough: Buat Dockerfile berisolasi berbasis image nvidia/cuda:12.1.0-runtime-ubuntu22.04. Pasang paket nvidia-container-toolkit pada host untuk memberikan akses GPU langsung ke container Docker. Konfigurasi ini memastikan Whisper.cpp dan Ollama mengeksekusi kernel matrix multiplication langsung di CUDA cores GPU.

Dengan mengimplementasikan arsitektur ini, Voice AI Agent lokal mampu menangani percakapan suara secara alami, responsif, hemat biaya, dan menjamin privasi data audio 100% di dalam infrastruktur sendiri.

๐Ÿ“– Artikel Terkait