AI-agenten · Les

Een voice-conversatielus bouwen

Opnemen → transcriberen → redeneren → spreken, met afhandeling van onderbrekingen.

Les 3 van 413 stappen

Een voice-conversatielus bouwen is een gratis AI-agenten-les op CoddyKit. Dit is les 3 van 4. Je kunt de volledige les hieronder gratis lezen en daarna in de browser praktisch oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject AI-agenten. Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus AI-agenten bevat in totaal 4 lessen.

De stemgesprekslus

Een volledige stemgesprekslus verbindt microfooningang en agentuitvoer in een doorlopende cyclus: opnemen → transcriberen → agent → TTS → afspelen → herhalen.

In deze les behandelen we elk onderdeel en de uitdagingen die stemlussen anders maken dan tekstgebaseerde agenten: stilte detecteren, interrupties afhandelen en het einde van de spraak detecteren.

Opnemen met de microfoon

Gebruik sounddevice om audio van de standaardmicrofoon vast te leggen. Neem op gedurende een vaste tijd of totdat stilte wordt gedetecteerd. Neem altijd op in mono met 16 kHz, de bemonsteringsfrequentie die Whisper verwacht.

Installeer het met pip install sounddevice soundfile numpy.

import sounddevice as sd
import numpy as np
import tempfile
import soundfile as sf

SAMPLE_RATE = 16000  # 16kHz mono — optimal for Whisper
DURATION = 5         # seconds

def record_audio(duration=DURATION, sample_rate=SAMPLE_RATE):
    print(f'Recording for {duration} seconds...')
    audio_data = sd.rec(
        int(duration * sample_rate),
        samplerate=sample_rate,
        channels=1,
        dtype='float32'
    )
    sd.wait()  # block until recording finishes
    print('Recording complete')
    return audio_data, sample_rate

def audio_to_file(audio_data, sample_rate, filepath='/tmp/recording.wav'):
    sf.write(filepath, audio_data, sample_rate)
    return filepath

Stilte detecteren aan het einde van de spraak

Opnemen gedurende een vaste tijd is omslachtig: gebruikers moeten wachten, ook als ze na 2 seconden al klaar zijn met spreken. Stiltedetectie stopt de opname automatisch wanneer de gebruiker gedurende een bepaalde drempelperiode stil is geweest.

import sounddevice as sd
import numpy as np
from collections import deque

SAMPLE_RATE = 16000
CHUNK_SIZE = 1024         # samples per chunk
SILENCE_THRESHOLD = 0.02  # RMS volume below this = silence
SILENCE_DURATION = 1.5   # seconds of silence to end recording
MAX_DURATION = 30         # max recording length in seconds

def record_until_silence():
    chunks = []
    silent_chunks = 0
    max_silent = int(SILENCE_DURATION * SAMPLE_RATE / CHUNK_SIZE)
    max_chunks = int(MAX_DURATION * SAMPLE_RATE / CHUNK_SIZE)

    print('Listening... (speak now)')
    with sd.InputStream(samplerate=SAMPLE_RATE, channels=1,
                        blocksize=CHUNK_SIZE, dtype='float32') as stream:
        while True:
            chunk, _ = stream.read(CHUNK_SIZE)
            chunks.append(chunk.copy())

            rms = np.sqrt(np.mean(chunk**2))
            if rms < SILENCE_THRESHOLD:
                silent_chunks += 1
            else:
                silent_chunks = 0  # speech detected — reset counter

            if silent_chunks >= max_silent and len(chunks) > max_silent:
                break
            if len(chunks) >= max_chunks:
                break

    audio = np.concatenate(chunks, axis=0)
    return audio, SAMPLE_RATE

De volledige stemlus

Verbind alle onderdelen in een doorlopende lus. Begin onmiddellijk opnieuw met luisteren nadat de agent heeft geantwoord, zodat een natuurlijk gesprek heen en weer ontstaat.

import openai
import os

client = openai.OpenAI(api_key=os.getenv('OPENAI_API_KEY'))

conversation_history = []

def voice_conversation_loop():
    print('Voice agent started. Say something (Ctrl+C to exit).')

    while True:
        # 1. Record user
        audio_data, sample_rate = record_until_silence()
        audio_path = audio_to_file(audio_data, sample_rate)

        # 2. Transcribe
        user_text = transcribe_file(audio_path)
        print(f'You: {user_text}')

        if not user_text.strip():
            continue  # empty — listen again

        # 3. Run agent
        conversation_history.append({'role': 'user', 'content': user_text})
        response = client.chat.completions.create(
            model='gpt-4o-mini',
            messages=[{'role': 'system', 'content': 'You are a helpful voice assistant. Keep responses concise.'}] + conversation_history[-10:]
        )
        agent_text = response.choices[0].message.content
        conversation_history.append({'role': 'assistant', 'content': agent_text})
        print(f'Agent: {agent_text}')

        # 4. TTS and play
        say(agent_text)  # speaks sentence by sentence

Het concept van wekwoorddetectie

Altijd luisteren is duur, vanwege doorlopende Whisper-aanroepen, en vormt een inbreuk op de privacy. Wekwoorddetectie gebruikt een lichtgewicht lokaal model dat volledige verwerking alleen activeert wanneer een specifieke zin wordt gehoord, zoals 'Hey Agent'.

# Wake word detection with pvporcupine (Picovoice)
# pip install pvporcupine sounddevice

import pvporcupine
import sounddevice as sd
import numpy as np
import os

def listen_for_wake_word(wake_word='computer'):
    porcupine = pvporcupine.create(
        access_key=os.getenv('PICOVOICE_KEY'),
        keywords=[wake_word]  # built-in: computer, hey barista, hey google, jarvis...
    )

    print(f'Listening for wake word: "{wake_word}"...')

    with sd.InputStream(
        samplerate=porcupine.sample_rate,
        channels=1,
        dtype='int16',
        blocksize=porcupine.frame_length
    ) as stream:
        while True:
            frame, _ = stream.read(porcupine.frame_length)
            pcm = frame.flatten().astype('int16')
            result = porcupine.process(pcm)
            if result >= 0:
                print(f'Wake word detected!')
                porcupine.delete()
                return True

Interrupties afhandelen

Gebruikers moeten de agent tijdens het spreken kunnen onderbreken. Implementeer interruptieafhandeling door het afspelen in een afzonderlijke thread uit te voeren en het microfoonvolume te controleren. Stop het afspelen onmiddellijk zodra de gebruiker begint te spreken.

import threading
import sounddevice as sd
import numpy as np

interrupt_event = threading.Event()

def monitor_for_interrupt(threshold=0.03):
    def audio_callback(indata, frames, time_info, status):
        rms = np.sqrt(np.mean(indata**2))
        if rms > threshold:
            print('Interrupt detected!')
            interrupt_event.set()

    with sd.InputStream(callback=audio_callback, channels=1,
                        samplerate=16000, blocksize=1024):
        while not interrupt_event.is_set():
            sd.sleep(50)

def speak_with_interrupt(text, voice='nova'):
    interrupt_event.clear()

    # Start interrupt monitor in background
    monitor_thread = threading.Thread(target=monitor_for_interrupt, daemon=True)
    monitor_thread.start()

    # Play audio sentence by sentence
    for sentence in split_into_sentences(text):
        if interrupt_event.is_set():
            print('Playback interrupted')
            break
        audio_path = cached_tts(sentence, voice)
        play_audio_file(audio_path)

Ruisfiltering

Microfoonaudio bevat vaak achtergrondruis, zoals ventilatoren, toetsenbordklikken en kamerecho. Pas een eenvoudige ruispoort toe om audio onder een drempelwaarde te onderdrukken en gebruik eventueel een stemactiviteitsdetector (VAD) voor een betere nauwkeurigheid.

import numpy as np

NOISE_GATE_THRESHOLD = 0.015  # RMS threshold

def apply_noise_gate(audio_data, threshold=NOISE_GATE_THRESHOLD):
    rms = np.sqrt(np.mean(audio_data**2))
    if rms < threshold:
        return np.zeros_like(audio_data)  # silence below threshold
    return audio_data

def has_speech_content(audio_data, threshold=0.02):
    rms_values = [
        np.sqrt(np.mean(audio_data[i:i+1600]**2))
        for i in range(0, len(audio_data), 1600)
    ]
    speech_frames = sum(1 for r in rms_values if r > threshold)
    speech_ratio = speech_frames / max(len(rms_values), 1)
    return speech_ratio > 0.1  # at least 10% of frames have speech

# Use in loop before transcribing
audio, sr = record_until_silence()
if has_speech_content(audio):
    text = transcribe_file(audio_to_file(audio, sr))
else:
    print('No speech detected — listening again')

Een toestandsmachine voor de stemgesprekslus

Een robuuste stemlus gebruikt een toestandsmachine om op elk moment bij te houden wat de agent doet en zo racecondities tussen afspelen en luisteren te voorkomen.

from enum import Enum

class AgentState(Enum):
    IDLE           = 'idle'
    LISTENING      = 'listening'
    TRANSCRIBING   = 'transcribing'
    THINKING       = 'thinking'
    SPEAKING       = 'speaking'

current_state = AgentState.IDLE

def set_state(new_state):
    global current_state
    print(f'State: {current_state.value} -> {new_state.value}')
    current_state = new_state

def voice_loop_with_state():
    while True:
        set_state(AgentState.LISTENING)
        audio, sr = record_until_silence()

        if not has_speech_content(audio):
            continue

        set_state(AgentState.TRANSCRIBING)
        text = transcribe_file(audio_to_file(audio, sr))
        if not text.strip():
            continue
        print(f'You: {text}')

        set_state(AgentState.THINKING)
        agent_response = run_agent(text)
        print(f'Agent: {agent_response}')

        set_state(AgentState.SPEAKING)
        speak_with_interrupt(agent_response)

Contextbeheer van gesprekken

De stemagent houdt de gespreksgeschiedenis bij, zodat die vervolgvragen kan beantwoorden, zoals "Vertel me daar meer over" of "Wat zei je ook alweer dat de prijs was?" Houd de geschiedenis in het geheugen en beperk deze tot de laatste N beurten om overloop van de context te voorkomen.

import openai
import os

client = openai.OpenAI(api_key=os.getenv('OPENAI_API_KEY'))

MAX_HISTORY_TURNS = 10
SYSTEM_PROMPT = 'You are a concise voice assistant. Keep responses under 3 sentences unless asked for detail.'

class VoiceAgent:
    def __init__(self):
        self.history = []

    def respond(self, user_text):
        self.history.append({'role': 'user', 'content': user_text})

        # Trim history to last N turns
        recent = self.history[-(MAX_HISTORY_TURNS * 2):]

        response = client.chat.completions.create(
            model='gpt-4o-mini',
            messages=[{'role': 'system', 'content': SYSTEM_PROMPT}] + recent
        )
        agent_text = response.choices[0].message.content
        self.history.append({'role': 'assistant', 'content': agent_text})
        return agent_text

    def reset(self):
        self.history = []
        print('Conversation history cleared')

Gecontroleerd afsluiten en opruimen

Handel Ctrl+C en andere afsluitsignalen gecontroleerd af: stop de audiostream, ruim tijdelijke bestanden op en neem afscheid voordat je afsluit.

import signal
import sys
import glob
import os

TMP_DIR = '/tmp'
agent = None

def cleanup_temp_files():
    for f in glob.glob(os.path.join(TMP_DIR, 'recording_*.wav')):
        os.unlink(f)
    print('Temp files cleaned up')

def graceful_shutdown(signum, frame):
    print('\nShutting down voice agent...')
    if agent:
        say('Goodbye! Have a great day.')
    cleanup_temp_files()
    sys.exit(0)

# Register signal handler
signal.signal(signal.SIGINT, graceful_shutdown)
signal.signal(signal.SIGTERM, graceful_shutdown)

# Main entry point
agent = VoiceAgent()
print('Voice agent ready. Press Ctrl+C to exit.')
voice_loop_with_state()

Het gesprek loggen

Houd een blijvend logboek van het gesprek bij met tijdstempels, getranscribeerde tekst, antwoorden van de agent en eventuele fouten. Dit is essentieel voor het opsporen van problemen met stemagenten en voor het bouwen van functies om gesprekken terug te bekijken.

import json
import os
from datetime import datetime

LOG_DIR = '/tmp/voice_agent_logs'
os.makedirs(LOG_DIR, exist_ok=True)

conversation_log = []

def log_turn(speaker, text, latency_ms=None, error=None):
    entry = {
        'timestamp': datetime.now().isoformat(),
        'speaker': speaker,   # 'user' or 'agent'
        'text': text,
        'latency_ms': latency_ms
    }
    if error:
        entry['error'] = error
    conversation_log.append(entry)

def save_conversation_log(session_id=None):
    if not session_id:
        session_id = datetime.now().strftime('%Y%m%d_%H%M%S')
    log_path = os.path.join(LOG_DIR, f'session_{session_id}.json')
    with open(log_path, 'w') as f:
        json.dump({
            'session_id': session_id,
            'turns': len(conversation_log),
            'log': conversation_log
        }, f, indent=2)
    print(f'Log saved: {log_path}')
    return log_path

# Usage in voice loop
log_turn('user', transcribed_text, latency_ms=420)
log_turn('agent', agent_response, latency_ms=850)

Kennischeck

Wat is het doel van stiltedetectie in een stemgesprekslus?

Samenvatting: een stemgesprekslus bouwen

Een stemgesprekslus: opnemen met de microfoon en stilte detecteren → transcriberen met Whisper → de agent uitvoeren met gespreksgeschiedenis → het TTS-antwoord per zin genereren → afspelen en interrupties controleren → herhalen.

Belangrijke onderdelen zijn een toestandsmachine om racecondities te voorkomen, wekwoorddetectie voor altijd luisteren, een ruispoort voor audiokwaliteit en het inkorten van de gespreksgeschiedenis voor contextbeheer. Handel Ctrl+C gecontroleerd af door op te ruimen en een afscheidsbericht af te spelen.

Gratis beginnen

Leer AI-agenten met een AI-tutor — gratis

Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.

Cursussen
60
Lessen
239

Veelgestelde vragen

Is de les “Een voice-conversatielus bouwen” gratis?

Ja — de volledige tekst van “Een voice-conversatielus bouwen” kun je hier gratis op het web lezen. Als je interactief wilt oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is, en de rest van de cursus AI-agenten wilt ontgrendelen, kun je upgraden naar CoddyKit PRO. De cursus AI-agenten bevat in totaal 4 lessen.

Wat leer ik in “Een voice-conversatielus bouwen”?

Opnemen → transcriberen → redeneren → spreken, met afhandeling van onderbrekingen. Je oefent met AI-agenten door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.

Heb ik ervaring nodig om met AI-agenten te beginnen?

Ervaring vooraf is niet nodig. AI-agenten op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 3 van 4.

Hoe lang duurt de les “Een voice-conversatielus bouwen”?

De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.

Kan ik code schrijven en uitvoeren in deze les over AI-agenten?

Ja. Elke les over AI-agenten bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.

Alle lessen in deze cursus

  1. Speech-to-text met Whisper en Deepgram
  2. Text-to-speech in agentreacties
  3. Een voice-conversatielus bouwen
  4. Latency optimaliseren voor voice-agents
← Terug naar AI-agenten