Categorieën bekijken

Spraak naar Tekst (STT)

Op deze pagina gaan we lokaal spraak naar tekst (Speech To Text) zetten om dit evt later naar een LLM door te sturen.

Het doel besturingssysteem is Windows (of WSL indien aangegeven)

Geluidsbestand naar tekst #

faster-whisper #

In dit voorbeeld genereren we tekst van een WAV of MP3 bestand, hiervoor heb je nodig: faster-whisper

Faster-Whisper Github: https://github.com/SYSTRAN/faster-whisper

Installeer faster-whisper met het commando:

pip install faster-whisper

Python script – STT met faster-whisper (CPU / NVIDIA) #

Gebruik onderstaande pythonscript om een WAV/MP3 bestand in tekst te zetten via de commandline: transcribe.py

from faster_whisper import WhisperModel
import sys

audio_file = sys.argv[1]

model_size = "base"  # tiny, base, small, medium, large-v3

model = WhisperModel(model_size, compute_type="int8")

segments, info = model.transcribe(audio_file)

print("Language:", info.language)
print("Duration:", info.duration)

for segment in segments:
    print(f"[{segment.start:.2f}s -> {segment.end:.2f}s] {segment.text}")

Nederlandse taal forceren (sneller)

Als je alleen NL verwacht:

segments, info = model.transcribe(audio_file, language="nl")

Dan hoeft Whisper geen language detection te doen.

GPU gebruiken ipv CPU en werkgeheugen (alleen nVIDIA / CUDA)

WhisperModel(
    "small",
    device="cuda",
    compute_type="float16"
)

Testbestanden

Hier heb ik een git gevonden met een aantal test bestanden:

https://github.com/voxserv/audio_quality_testing_samples

Bijvoorbeeld:

https://github.com/voxserv/audio_quality_testing_samples/blob/master/testaudio/8000/test01_20s.wav

Start het STT python script:

python3 transcribe.py test01_20s.wav

Output:

Language: en
Duration: 24.0
[0.00s -> 8.52s]  Dancing in the masquerade, idle truth and plain sight jaded, pop, roll, click, shot.
[8.52s -> 10.64s]  Who will I be today or not?
[10.64s -> 15.56s]  But such a tide as moving seems asleep, too full for sound and foam.
[15.56s -> 20.56s]  When that witch drew from out the boundless deep turns again home, twilight and evening
[20.56s -> 22.56s]  bell, and after that,

Het bestand uit die git “165187__blaukreuz__global-village-hochdeutsch.wav”, geeft:

Language: de
Duration: 24.92
[0.00s -> 2.80s]  Vor dem Gesetz steht ein Türhüter.
[2.80s -> 8.16s]  Zu diesem Türhüter kommt ein Mann vom Lande und bittet um Eintritt in das Gesetz.
[8.16s -> 14.08s]  Aber der Türhüter sagt, dass er ihm jetzt den Eintritt nicht gewähren könne.
[14.08s -> 19.96s]  Der Mann überlegt und fragt dann, ob er also später werde Eintreten dürfen.
[19.96s -> 24.80s]  Es ist möglich, sagt der Türhüter, jetzt aber nicht.

faster-whisper modellen kiezen #

modelRAMsnelheid
tiny~1GBsuper snel
base~1GBsnel
small~2GBbeter
medium~5GBgoed
large-v3~10GBbeste

Waar worden deze modellen neergezet (download)?

Bij Faster-Whisper worden de modellen automatisch gedownload via de HuggingFace cache. In Windows komen ze standaard hier terecht:

C:\Users\<USERNAME>\.cache\huggingface\hub\

Bijvoorbeeld:

C:\Users\Sebastiaan\.cache\huggingface\hub\models--Systran--faster-whisper-base

Daar zie je vaak:

models--Systran--faster-whisper-base
models--Systran--faster-whisper-small
models--Systran--faster-whisper-large-v3

Binnen zo’n map:

snapshots\
refs\
blobs\

De snapshots map bevat het echte model dat CTranslate2 gebruikt.

Grootte van Faster-Whisper modellen

modelapprox sizeRAM gebruik
tiny~75 MB~300-500 MB
base~150 MB~500-700 MB
small~480 MB~1-2 GB
medium~1.5 GB~3-4 GB
large-v3~3.1 GB~6-8 GB

De base van Faster-Whisper is verrassend klein.

Dat komt omdat Faster-Whisper geconverteerde CTranslate2 modellen gebruikt i.p.v. de originele PyTorch modellen van OpenAI Whisper.

Hierdoor:

  • model is gequantized
  • inference engine is C++
  • veel minder overhead

Waarom zo klein?

De originele Whisper base: ~500 MB

Faster-Whisper base: ~150 MB

Door: FP16 / INT8 quantization + CTranslate2 format + weight packing

Dus zeg maar: wat GGUF is voor taalmodellen is CTranslate2 voor audio modellen.

Waarom Faster-Whisper zo snel is

Omdat CTranslate2:

  • SIMD optimalisaties
  • AVX / AVX2 / AVX512
  • GPU kernels
  • INT8 quantization
  • streaming inference

heeft.

Daarom zie je vaak:

faster-whisper = 4x sneller dan whisper


whisper.cpp (CPU / NVIDIA) #

Github: https://github.com/ggml-org/whisper.cpp

Een ander programma “kant-en-klaar” voor windows is whisper.cpp, download een Windows binairy zoals whisper-blas-bin-x64.zip en pak deze uit.

Modellen voor whisper.cpp

Je kan de modellen voor whisper.cpp vinden op huggingface: https://huggingface.co/ggerganov/whisper.cpp/tree/main

Download bijvoorbeeld “ggml-base.bin” en plaats deze in dezelfde folder als waar de bestanden van whisper.cpp staan.

Note: bestanden zonder toevoegingen zijn F16

Performance verschil op CPU:

modelsnelheid
FP16baseline
q8~1.3× sneller
q5~1.6× sneller

Kwaliteit: Voor speech recognition blijft het verrassend goed.

quantkwaliteit
FP16perfect
q8vrijwel identiek
q5klein beetje verlies

De officiële Whisper modellen

Dit zijn de originele modellen van OpenAI:

ModelParametersGebruik
tiny39Mextreem snel
base74Mlicht
small244Mrealtime
medium769Mgoede accuracy
large1.55Bbeste accuracy

Daar bovenop kwamen later:

ModelOpmerking
large-v2verbeterde training
large-v3huidige beste
large-v3-turbosneller

Voor nu een commando uit om een audiobestand naar tekst te zetten:

whisper-cli -m ggml-base.bin -f test01_20s.wav

Output:

whisper_init_from_file_with_params_no_state: loading model from 'ggml-base-q8_0.bin'
whisper_init_with_params_no_state: use gpu    = 1
whisper_init_with_params_no_state: flash attn = 1
whisper_init_with_params_no_state: gpu_device = 0
whisper_init_with_params_no_state: dtw        = 0
whisper_init_with_params_no_state: devices    = 2
whisper_init_with_params_no_state: backends   = 2
whisper_model_load: loading model
whisper_model_load: n_vocab       = 51865
whisper_model_load: n_audio_ctx   = 1500
whisper_model_load: n_audio_state = 512
whisper_model_load: n_audio_head  = 8
whisper_model_load: n_audio_layer = 6
whisper_model_load: n_text_ctx    = 448
whisper_model_load: n_text_state  = 512
whisper_model_load: n_text_head   = 8
whisper_model_load: n_text_layer  = 6
whisper_model_load: n_mels        = 80
whisper_model_load: ftype         = 7
whisper_model_load: qntvr         = 2
whisper_model_load: type          = 2 (base)
whisper_model_load: adding 1608 extra tokens
whisper_model_load: n_langs       = 99
whisper_model_load:          CPU total size =    81.18 MB
whisper_model_load: model size    =   81.18 MB
whisper_backend_init_gpu: device 0: BLAS (type: 3)
whisper_backend_init_gpu: device 1: CPU (type: 0)
whisper_backend_init_gpu: no GPU found
whisper_backend_init: using BLAS backend
whisper_init_state: kv self size  =    6.29 MB
whisper_init_state: kv cross size =   18.87 MB
whisper_init_state: kv pad  size  =    3.15 MB
whisper_init_state: compute buffer (conv)   =   16.28 MB
whisper_init_state: compute buffer (encode) =   23.09 MB
whisper_init_state: compute buffer (cross)  =    4.66 MB
whisper_init_state: compute buffer (decode) =   96.37 MB

system_info: n_threads = 4 / 32 | WHISPER : COREML = 0 | OPENVINO = 0 | CPU : SSE3 = 1 | SSSE3 = 1 | AVX = 1 | AVX2 = 1 | F16C = 1 | FMA = 1 | OPENMP = 1 | REPACK = 1 |

main: processing 'test01_20s.wav' (384000 samples, 24.0 sec), 4 threads, 1 processors, 5 beams + best of 5, lang = en, task = transcribe, timestamps = 1 ...

[00:00:00.000 --> 00:00:08.460]   Dancing in the masquerade, idle truth and plain sight jaded, pop, roll, click, shot.
[00:00:08.460 --> 00:00:10.660]   Who will I be today or not?
[00:00:10.660 --> 00:00:16.240]   But such a tide as moving seems asleep, too full for sound and foam, when that witch
[00:00:16.240 --> 00:00:21.680]   drew from out the boundless deep turns again home, twilight and evening bell and after
[00:00:21.680 --> 00:00:21.980]   that.

whisper_print_timings:     load time =    66.37 ms
whisper_print_timings:     fallbacks =   0 p /   0 h
whisper_print_timings:      mel time =     9.52 ms
whisper_print_timings:   sample time =   168.87 ms /   408 runs (     0.41 ms per run)
whisper_print_timings:   encode time =   704.81 ms /     1 runs (   704.81 ms per run)
whisper_print_timings:   decode time =     0.00 ms /     1 runs (     0.00 ms per run)
whisper_print_timings:   batchd time =   533.88 ms /   406 runs (     1.31 ms per run)
whisper_print_timings:   prompt time =     0.00 ms /     1 runs (     0.00 ms per run)
whisper_print_timings:    total time =  1505.57 ms


Opname via een microfoon #

Er zijn veel programma’s die kunnen opnemen van een microfoon, wat we willen is streaming audio, het liefst via een server IP/poort, de kortste klap is om ffmpeg te gebruiken

FFmpeg #


Installatie van ffmpeg, als je winget hebt (Windows 10/11):

winget install ffmpeg




Handmatig installeren

  1. Download build:
    https://www.gyan.dev/ffmpeg/builds/
  2. Neem:
ffmpeg-release-essentials.zip
  1. Uitpakken naar bijvoorbeeld:
C:\ffmpeg
  1. Voeg toe aan PATH:
C:\ffmpeg\bin

Dan werkt overal:

ffmpeg
ffplay
ffprobe

Controleer beschikbare apparaten met:

ffmpeg -list_devices true -f dshow -i dummy

Voorbeeld output:

[dshow @ 0000028030ff5540] "Logitech Webcam C930e" (video)
[dshow @ 0000028030ff5540] "Microfoon (Logitech Webcam C930e)" (audio)
[dshow @ 0000028030ff5540] "What U Hear (3- Sound BlasterX AE-5)" (audio)
[dshow @ 0000028030ff5540] "Microphone (Yeti Stereo Microphone)" (audio)

Opname stream starten #

Om bijvoorbeeld een opname te starten via TCP (alle devices):

ffmpeg -f dshow -i audio="Microfoon (Logitech Webcam C930e)" -ac 1 -ar 16000 -f wav tcp://0.0.0.0:9999

RAW PCM / Localhost:

ffmpeg -f dshow -i audio="Microfoon (Logitech Webcam C930e)" -ac 1 -ar 16000 -f s16le tcp://127.0.0.1:9999?listen=1

Zonder foutmeldingen (als er bijvoorbeeld geen clients is verbonden en de buffer wordt te vol):

ffmpeg -loglevel error -f dshow -i audio="Microfoon (Logitech Webcam C930e)" -ac 1 -ar 16000 -f s16le tcp://127.0.0.1:9999?listen=1

Via UDP:

ffmpeg -loglevel error -f dshow -i audio="Microfoon (Logitech Webcam C930e)" -ac 1 -ar 16000 -f s16le udp://127.0.0.1:9999

whisper.cpp STT via commandline #

Nu kan je met één commando een stream starten met ffmpeg en deze d.m.v. een pipe doorzetten naar whisper.cpp:

ffmpeg -loglevel error -f dshow -i audio="Microfoon (Logitech Webcam C930e)" -ac 1 -ar 16000 -f s16le - | whisper-stream.exe -m ggml-base.bin

Maar het model heeft wel moeite met de nederlandse taal:

  • gebruik een beter model: ggml-small-q8_0.bin
  • voeg de taal toe: -l nl
  • voeg een treshold toe om ruis uit te filteren: -vth 0.6

-vth  (voice activity threshold)

In whisper.cpp betekent:

waardegedrag
0.3zeer gevoelig
0.6default
0.8alleen duidelijke spraak
0.9bijna geen ruis meer

ffmpeg -loglevel error -f dshow -i audio="Microfoon (Logitech Webcam C930e)" -ac 1 -ar 16000 -f s16le - | whisper-stream.exe -m ggml-small-q8_0.bin -l nl -vth 0.6

Er is nog steeds kans op buffer overflow vanuit ffmpeg, zet de buffer uit, en stream “live”.

ffmpeg -loglevel error -f dshow -fflags nobuffer -i audio="Microfoon (Logitech Webcam C930e)" -ac 1 -ar 16000 -f s16le - | whisper-stream.exe -m ggml-small-q8_0.bin -l nl -t 16 -vth 0.6

  • Voeg eventueel meer CPU threads to met: -t 16
  • Laat een kleine buffer toe 64M: -rtbufsize 64M -f dshow
ffmpeg -loglevel error -fflags nobuffer -rtbufsize 64M -f dshow -i audio="Microfoon (Logitech Webcam C930e)" -ac 1 -ar 16000 -f s16le - | whisper-stream.exe -m ggml-small-q8_0.bin -l nl -t 16 -vth 0.6

Om nog meer ruis te onderdrukken

  • voeg een filter toe bij ffmpeg: -af highpass=f=120
  • verhoog de voice activity threshold: -vth 0.8
ffmpeg -loglevel error -fflags nobuffer -rtbufsize 64M -f dshow -i audio="Microfoon (Logitech Webcam C930e)" -af highpass=f=120 -ac 1 -ar 16000 -f s16le - | whisper-stream.exe -m ggml-small-q8_0.bin -l nl -t 16 -vth 0.8

whisper.cpp STT compileren voor VULKAN support (AMD) #

De standaard build van whisper.cpp ondersteunt geen VULKAN voor AMD kaarten, maar dat kunnen we zelf compileren vanuit de source!

github: https://github.com/DomoticX/whisper.cpp-windows-vulkan

Vulkan SDK installeren #

Download: https://vulkan.lunarg.com/sdk/home

Neem: Windows x64 Installer (bijvoorbeeld vulkansdk-windows-X64-1.x.xxx.x.exe)

Tijdens installatie:

  • Install everything
  • Environment variables laten zetten

Start een Windows command en typ:

echo %VULKAN_SDK%

Je zou iets moeten zien zoals:

C:\VulkanSDK\1.3.xxx.x

Installeer CMake (compiler) #

In Windows command:

winget install Kitware.CMake

Output (voorbeeld)

Found CMake [Kitware.CMake] Version 4.2.3
This application is licensed to you by its owner.
Microsoft is not responsible for, nor does it grant any licenses to, third-party packages.
Downloading https://github.com/Kitware/CMake/releases/download/v4.2.3/cmake-4.2.3-windows-x86_64.msi
  ██████████████████████████████  34.7 MB / 34.7 MB
Successfully verified installer hash
Starting package install...
Successfully installed

Installeer whisper.cpp source (GIT) #

In Windows command:

git clone https://github.com/ggml-org/whisper.cpp.git

Output voorbeeld:

Cloning into 'whisper.cpp'...
remote: Enumerating objects: 31665, done.
remote: Counting objects: 100% (149/149), done.
remote: Compressing objects: 100% (75/75), done.
remote: Total 31665 (delta 79), reused 74 (delta 74), pack-reused 31516 (from 4)
Receiving objects: 100% (31665/31665), 33.63 MiB | 23.60 MiB/s, done.
Resolving deltas: 100% (23050/23050), done.

Bouw whisper.cpp binaries (VULKAN) #

Ga naar de source folder:

cd whisper.cpp

Optioneel als je wilt experimenteren of telkens nieuwe builds wil maken, verwijder oude build:

rmdir /s /q build

Je kan nu de make configure bouwen met de flag: -DGGML_VULKAN=1

cmake -B build -DGGML_VULKAN=1

CPUs zonder AVX512 kunnen crashen!

Bijvoorbeeld:

CPUAVX512
Intel 12/13 gen
AMD Ryzen
Xeon / Threadrippersoms


Dus voor maximale compatibiliteit kun je beter bouwen met: -DGGML_AVX512=OFF

cmake -B build -DGGML_VULKAN=1 -DGGML_AVX512=OFF

Wat nog steeds automatisch actief blijft

Zelfs zonder AVX512 gebruikt ggml nog:

- AVX2
- FMA
- OpenMP

Dat zie je ook in je build log, dus performance blijft goed.

Output voorbeeld:

-- Building for: Visual Studio 18 2026
CMake Deprecation Warning at CMakeLists.txt:1 (cmake_minimum_required):
  Compatibility with CMake < 3.10 will be removed from a future version of
  CMake.

  Update the VERSION argument <min> value.  Or, use the <min>...<max> syntax
  to tell CMake that the project requires at least <min> but has been updated
  to work with policies introduced by <max> or earlier.


-- Selecting Windows SDK version 10.0.26100.0 to target Windows 10.0.26200.
-- The C compiler identification is MSVC 19.50.35726.0
-- The CXX compiler identification is MSVC 19.50.35726.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: C:/Program Files (x86)/Microsoft Visual Studio/18/BuildTools/VC/Tools/MSVC/14.50.35717/bin/Hostx64/x64/cl.exe - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: C:/Program Files (x86)/Microsoft Visual Studio/18/BuildTools/VC/Tools/MSVC/14.50.35717/bin/Hostx64/x64/cl.exe - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Found Git: C:/Program Files/Git/cmd/git.exe (found version "2.53.0.windows.1")
-- The ASM compiler identification is MSVC
CMake Warning (dev) at C:/Program Files/CMake/share/cmake-4.2/Modules/CMakeDetermineASMCompiler.cmake:234 (message):
  Policy CMP194 is not set: MSVC is not an assembler for language ASM.  Run
  "cmake --help-policy CMP194" for policy details.  Use the cmake_policy
  command to set the policy and suppress this warning.
Call Stack (most recent call first):
  ggml/CMakeLists.txt:2 (project)
This warning is for project developers.  Use -Wno-dev to suppress it.

-- Found assembler: C:/Program Files (x86)/Microsoft Visual Studio/18/BuildTools/VC/Tools/MSVC/14.50.35717/bin/Hostx64/x64/cl.exe
-- Performing Test CMAKE_HAVE_LIBC_PTHREAD
-- Performing Test CMAKE_HAVE_LIBC_PTHREAD - Failed
-- Looking for pthread_create in pthreads
-- Looking for pthread_create in pthreads - not found
-- Looking for pthread_create in pthread
-- Looking for pthread_create in pthread - not found
-- Found Threads: TRUE
-- Warning: ccache not found - consider installing it for faster compilation or disable this warning with GGML_CCACHE=OFF
-- CMAKE_SYSTEM_PROCESSOR: AMD64
-- CMAKE_GENERATOR_PLATFORM:
-- GGML_SYSTEM_ARCH: x86
-- Including CPU backend
-- Found OpenMP_C: -openmp (found version "2.0")
-- Found OpenMP_CXX: -openmp (found version "2.0")
-- Found OpenMP: TRUE (found version "2.0")
-- x86 detected
-- Performing Test HAS_AVX_1
-- Performing Test HAS_AVX_1 - Success
-- Performing Test HAS_AVX2_1
-- Performing Test HAS_AVX2_1 - Success
-- Performing Test HAS_FMA_1
-- Performing Test HAS_FMA_1 - Success
-- Performing Test HAS_AVX512_1
-- Performing Test HAS_AVX512_1 - Success
-- Adding CPU backend variant ggml-cpu: /arch:AVX512 GGML_AVX512
-- Found Vulkan: C:/VulkanSDK/1.4.341.1/Lib/vulkan-1.lib (found version "1.4.341") found components: glslc glslangValidator
-- Vulkan found
-- GL_KHR_cooperative_matrix supported by glslc
-- GL_NV_cooperative_matrix2 supported by glslc
-- GL_EXT_integer_dot_product supported by glslc
-- GL_EXT_bfloat16 supported by glslc
-- Including Vulkan backend
-- ggml version: 0.9.7
-- ggml commit:  30c5194c
-- Configuring done (21.4s)
-- Generating done (0.3s)
-- Build files have been written to: E:/whisper.cpp/build

Nadat make configure is voltooid begin nu met bouwen van de binaries:

cmake --build build --config Release

Nadat dit hele proces voltooid is vind je de binaries in: \build\bin\Release\

Gegenereerde bestanden:

whisper-vad-speech-segments.exe
whisper-server.exe
whisper-quantize.exe
whisper-cli.exe
whisper-bench.exe
test-vad-full.exe
test-vad.exe
main.exe
whisper.dll
ggml.dll
ggml-vulkan.dll
ggml-cpu.dll
ggml-base.dll
bench.exe

Waar is whisper-stream.exe gebleven?


In oudere builds zat:

examples/stream/

Die gebruikte PortAudio / SDL voor microfoon capture.

Die wordt tegenwoordig vaak niet meer standaard gebouwd, omdat:

  • OS audio capture verschilt per platform
  • veel mensen toch ffmpeg pipelines gebruiken

whisper.cpp server #

Je hebt nu ook de beschikking over de whisper server, je eigen STT endpoint!

Start de whisper server bijvoorbeeld met poort 9090:

whisper-server.exe -m ggml-small-q8_0.bin --port 9090

En stuur bijvoorbeeld vanuit een andere console window, een bestand met CURL POST naar de eindpoint /inference (test01_20s.wav)

curl -X POST http://127.0.0.1:9090/inference -F "file=@test01_20s.wav"

Je krijgt dan een JSON output terug (voorbeeld):

{"text":" Dancing in the masquerade, idle truth in plain sight jaded, pop, roll, click, shot,\n who will I be today or not?\n But such a tide as moving seems asleep, too full for sound and foam, when that which drew\n from out the boundless deep turns again home, twilight and evening bell and after that,\n [BLANK_AUDIO]\n"}

Realtime met whisper.cpp server en ffmpeg microphone wav chunks #

Nu er geen streaming exe bestaat moeten we het doen met de toosl die we hebben, als je zeg maar een audiostream start vanaf ffmpeg udp, dan is de stream onafgebroken en de whisper-server verwacht een “einde audio” alvorens hij een json uitput geeft. Ik heb samen met chatGPT dit enigszins werkend gekregen met behulp van een BAT/CMD bestand (zie ondeR), dit laat ffmpeg opnemen in chunk.wav bestanden en stuurt die stuk voor stuk naar de whisper-server op poort 9090 om de audio naar tekst om te zetten:

stt.cmd

@echo off

echo Starting microphone capture...

start "" ffmpeg -loglevel error -f dshow -i audio="Microphone (Yeti Stereo Microphone)" -ac 1 -ar 16000 -f segment -segment_time 2 -segment_format wav -reset_timestamps 1 chunk_%%03d.wav


echo Waiting for audio chunks...

:loop

for %%f in (chunk_*.wav) do (

    timeout /t 1 >nul

    echo Processing %%f
    curl http://127.0.0.1:9090/inference -F "file=@%%f"
    echo.
    del %%f
)

timeout /t 1 >nul
goto loop

in de console met de server output zie je bijvoorbeeld:

Running whisper.cpp inference on chunk_034.wav
Received request: chunk_035.wav
Successfully loaded chunk_035.wav

system_info: n_threads = 4 / 32 | WHISPER : COREML = 0 | OPENVINO = 0 | CPU : SSE3 = 1 | SSSE3 = 1 | AVX = 1 | AVX2 = 1 | F16C = 1 | FMA = 1 | AVX512 = 1 | OPENMP = 1 | REPACK = 1 |

operator (): processing 'chunk_035.wav' (80000 samples, 5.0 sec), 4 threads, 1 processors, lang = nl, task = transcribe, timestamps = 1 ...

Running whisper.cpp inference on chunk_035.wav
Received request: chunk_036.wav
Successfully loaded chunk_036.wav

system_info: n_threads = 4 / 32 | WHISPER : COREML = 0 | OPENVINO = 0 | CPU : SSE3 = 1 | SSSE3 = 1 | AVX = 1 | AVX2 = 1 | F16C = 1 | FMA = 1 | AVX512 = 1 | OPENMP = 1 | REPACK = 1 |

operator (): processing 'chunk_036.wav' (80000 samples, 5.0 sec), 4 threads, 1 processors, lang = nl, task = transcribe, timestamps = 1 ...

Running whisper.cpp inference on chunk_036.wav
Received request: chunk_037.wav
Successfully loaded chunk_037.wav

system_info: n_threads = 4 / 32 | WHISPER : COREML = 0 | OPENVINO = 0 | CPU : SSE3 = 1 | SSSE3 = 1 | AVX = 1 | AVX2 = 1 | F16C = 1 | FMA = 1 | AVX512 = 1 | OPENMP = 1 | REPACK = 1 |

operator (): processing 'chunk_037.wav' (80000 samples, 5.0 sec), 4 threads, 1 processors, lang = nl, task = transcribe, timestamps = 1 ...

Running whisper.cpp inference on chunk_037.wav

En in de andere console zie je de tekst:

Processing chunk_000.wav
{"text":""}
Processing chunk_001.wav
{"text":" Hallo, hallo, hoe gaat het hier?\n"}
Processing chunk_002.wav
{"text":""}
Processing chunk_003.wav
{"text":" Het gaat prima hier!\n"}
Processing chunk_004.wav
...

Realtime met whisper.cpp server en ffmpeg microphone wav chunks #

Een mooiere oplossing is om met python direct de audiochunk in het geheugen te laden en door te sturen naar de server, ChatGPT, kwam met deze python code.

Benodigheden:

pip install sounddevice numpy requests

stt.py

import io
import json
import time
import wave
from queue import Queue

import numpy as np
import requests
import sounddevice as sd

# ===== Instellingen =====
SERVER_URL = "http://127.0.0.1:9090/inference"
SAMPLERATE = 16000
CHANNELS = 1
BLOCK_SECONDS = 1.0          # 1.0 = lage latency, 0.5 kan ook
DEVICE = None                # None = standaard microfoon
NOISE_GATE = 250             # simpele stiltefilter op RMS-niveau
PRINT_EMPTY = False          # True om ook lege responses te tonen
REQUEST_TIMEOUT = 15

# ===== Interne queue =====
audio_queue: Queue[np.ndarray] = Queue()


def pcm16_to_wav_bytes(audio_int16: np.ndarray, samplerate: int) -> bytes:
    """Zet int16 numpy audio om naar WAV-bytes in geheugen."""
    buf = io.BytesIO()
    with wave.open(buf, "wb") as wf:
        wf.setnchannels(1)
        wf.setsampwidth(2)  # int16 = 2 bytes
        wf.setframerate(samplerate)
        wf.writeframes(audio_int16.tobytes())
    return buf.getvalue()


def rms_level(audio_int16: np.ndarray) -> float:
    """Eenvoudige geluidssterkte-inschatting."""
    audio_f32 = audio_int16.astype(np.float32)
    return float(np.sqrt(np.mean(audio_f32 * audio_f32)))


def audio_callback(indata, frames, time_info, status):
    if status:
        print(f" {status}")
    # Kopie maken zodat sounddevice buffer veilig vrijgegeven kan worden
    audio_queue.put(indata.copy().reshape(-1))


def post_chunk(audio_int16: np.ndarray) -> str:
    wav_bytes = pcm16_to_wav_bytes(audio_int16, SAMPLERATE)

    files = {
        "file": ("chunk.wav", wav_bytes, "audio/wav")
    }

    response = requests.post(
        SERVER_URL,
        files=files,
        timeout=REQUEST_TIMEOUT,
    )
    response.raise_for_status()

    data = response.json()
    return data.get("text", "").strip()


def main():
    print("Live STT gestart.")
    print(f"Server: {SERVER_URL}")
    print(f"Sample rate: {SAMPLERATE} Hz")
    print(f"Block size: {BLOCK_SECONDS:.2f} s")
    print("Spreek maar... Ctrl+C om te stoppen.\n")

    block_samples = int(SAMPLERATE * BLOCK_SECONDS)
    collected = np.empty(0, dtype=np.int16)

    with sd.InputStream(
        samplerate=SAMPLERATE,
        channels=CHANNELS,
        dtype="int16",
        callback=audio_callback,
        device=DEVICE,
        blocksize=0,
    ):
        while True:
            chunk = audio_queue.get()
            collected = np.concatenate((collected, chunk.astype(np.int16)))

            while len(collected) >= block_samples:
                block = collected[:block_samples]
                collected = collected[block_samples:]

                level = rms_level(block)
                if level < NOISE_GATE:
                    if PRINT_EMPTY:
                        print("[stilte]")
                    continue

                started = time.perf_counter()
                try:
                    text = post_chunk(block)
                    elapsed_ms = int((time.perf_counter() - started) * 1000)

                    if text:
                        print(f"[{elapsed_ms} ms] {text}")
                    elif PRINT_EMPTY:
                        print(f"[{elapsed_ms} ms] [geen tekst]")
                except requests.RequestException as e:
                    print(f"[HTTP fout] {e}")
                    time.sleep(0.5)
                except json.JSONDecodeError:
                    print("[fout] Server gaf geen geldige JSON terug.")
                    time.sleep(0.5)


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\nGestopt.")

Start de server en start in een andere console:

python3 stt.py

Nu heb je echte realtime transcriptie.


Voice Activity Detection (VAD) #

De volgende stap is het toepassen van VAD (Voice Activity Detection)

Het betekent simpelweg: detecteren of iemand spreekt of niet

Dus het algoritme beslist continu:

audio → spraak
audio → stilte / ruis

Resultaat:

0 = geen spraak
1 = spraak

Dat gebruik je om:

  • audio te segmenteren
  • alleen spraak naar STT te sturen
  • latency te verlagen
  • CPU/GPU te besparen

Twee populaire VAD engines #

webrtcvad (WebRTC) #

Dit is de klassieke VAD van Google WebRTC.

Voordelen:

  • extreem licht
  • realtime
  • werkt op CPU
  • gebruikt in Zoom / Meet / Discord

Eigenschappen:

10 / 20 / 30 ms audio frames

Dus het analyseert audio 100× per seconde.

Python voorbeeld:

import webrtcvadvad = webrtcvad.Vad(2)if vad.is_speech(frame, 16000):
print("speech")

Modes:

modegevoeligheid
0laag
1medium
2hoog
3zeer hoog

silero-vad #

Dit is een neuraal netwerk VAD.

Dus:

audio → AI model → spraak detectie

Voordelen:

  • veel nauwkeuriger
  • beter bij achtergrondgeluid
  • werkt goed met Whisper

Nadelen:

  • iets zwaarder


Het draait vaak met:

ONNX + PyTorch

Verschil #

VADtypeCPUnauwkeurigheid
webrtcvadklassiek DSPzeer laaggoed
silero-vadAI modellaagzeer goed

VAD – Python script 1 #

Met dit script luister je naar een TCP stream, bijvoorbeeld gegenereerd met ffmpeg.

Je hebt nodig: webrtcvad, instelleer deze met: pip install webrtcvad

Collecting webrtcvad
  Using cached webrtcvad-2.0.10.tar.gz (66 kB)
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
  Preparing metadata (pyproject.toml) ... done
Building wheels for collected packages: webrtcvad
  Building wheel for webrtcvad (pyproject.toml) ... done
  Created wheel for webrtcvad: filename=webrtcvad-2.0.10-cp311-cp311-win_amd64.whl size=18430 sha256=3e1eb82b284aff4eedf9eacbb541de6614d8ced6dd317eafeb960c92c5389735
  Stored in directory: c:\users\orka2\appdata\local\pip\cache\wheels\94\65\3f\292d0b656be33d1c801831201c74b5f68f41a2ae465ff2ee2f
Successfully built webrtcvad
Installing collected packages: webrtcvad
Successfully installed webrtcvad-2.0.10

Python script voor VAD:

import socket
import webrtcvad

HOST = "127.0.0.1"
PORT = 9999

RATE = 16000
FRAME_MS = 20
FRAME_BYTES = int(RATE * FRAME_MS / 1000) * 2

vad = webrtcvad.Vad(2)

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((HOST, PORT))

buffer = b''

print("connected to stream")

while True:

    data = sock.recv(4096)
    buffer += data

    while len(buffer) >= FRAME_BYTES:

        frame = buffer[:FRAME_BYTES]
        buffer = buffer[FRAME_BYTES:]

        if vad.is_speech(frame, RATE):
            print("speech detected")

VAD – Python script 2 #

Zoals je merkt en test gaat het prima, alles is het wat onoverzichtelijk als je telkens dezelfde regel ziet staan op de commandline, daarvoor deze update met frame en speech counter:

import socket
import webrtcvad

HOST = "127.0.0.1"
PORT = 9999

RATE = 16000
FRAME_MS = 20
FRAME_BYTES = int(RATE * FRAME_MS / 1000) * 2

vad = webrtcvad.Vad(2)

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((HOST, PORT))

buffer = b''

frame_counter = 0
speech_counter = 0

print("Connected to audio stream")

while True:

    data = sock.recv(4096)
    buffer += data

    while len(buffer) >= FRAME_BYTES:

        frame = buffer[:FRAME_BYTES]
        buffer = buffer[FRAME_BYTES:]

        frame_counter += 1

        if vad.is_speech(frame, RATE):
            speech_counter += 1
            print(f"frame {frame_counter} | speech #{speech_counter}")

Maar telkens als je dit python script afsluit zie je bij ffmpeg de volgende foutmelding (voorbeeld:

[aost#0:0/pcm_s16le @ 0000015f51980d40] Error submitting a packet to the muxer: Error number -10054 occurred
    Last message repeated 1 times
[out#0/s16le @ 0000015f51969000] Error muxing a packet
[out#0/s16le @ 0000015f51969000] Task finished with error code: -10054 (Error number -10054 occurred)
[out#0/s16le @ 0000015f51969000] Terminating thread with return code -10054 (Error number -10054 occurred)
[out#0/s16le @ 0000015f51969000] Error writing trailer: Error number -10054 occurred
[out#0/s16le @ 0000015f51969000] Error closing file: Error number -10054 occurred

dat gedrag is normaal. Wat er gebeurt:

ffmpeg TCP server

python client connected

python stopt

TCP socket sluit

ffmpeg kan niet meer schrijven

ffmpeg stopt met error -10054

Error -10054 betekent in Windows:

WSAECONNRESET

connection reset by peer

Dus simpel gezegd:

client disconnect → server stopt

Oplossing: gebruik UDP i.p.v. TCP.

Dan maakt het niet uit of de client stopt.


VAD – Python script 3 (UDP) #

Start ffmpeg stream via UDP (voorbeeld):

ffmpeg -loglevel error -f dshow -i audio="Microfoon (Logitech Webcam C930e)" -ac 1 -ar 16000 -f s16le udp://127.0.0.1:9999

Python script om UDP audiostream te verwerken met VAD:

import socket
import webrtcvad

HOST = "127.0.0.1"
PORT = 9999

RATE = 16000
FRAME_MS = 20
FRAME_BYTES = int(RATE * FRAME_MS / 1000) * 2

vad = webrtcvad.Vad(3)

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("127.0.0.1",9999))

buffer = b''

frame_counter = 0
speech_counter = 0

print("Connected to audio stream")

while True:

    data = sock.recv(4096)
    buffer += data

    while len(buffer) >= FRAME_BYTES:

        frame = buffer[:FRAME_BYTES]
        buffer = buffer[FRAME_BYTES:]

        frame_counter += 1

        if vad.is_speech(frame, RATE):
            speech_counter += 1
            print(f"frame {frame_counter} | speech #{speech_counter}")

Voorbeeld output:

frame 968 | speech #155
frame 976 | speech #156
frame 977 | speech #157
frame 978 | speech #158
frame 979 | speech #159
frame 1006 | speech #160
frame 1007 | speech #161
frame 1008 | speech #162
frame 1009 | speech #163
frame 1010 | speech #164
frame 38436 | speech #165
frame 38437 | speech #166
frame 38438 | speech #167
frame 38439 | speech #168
frame 38443 | speech #169
frame 38444 | speech #170
frame 38445 | speech #171
frame 38446 | speech #172

De vier niveaus (webrtcvad.Vad([niveau])

modegevoeligheidgedrag
0laaglaat veel door
1normaalredelijk tolerant
2agressieffiltert meer ruis
3zeer agressiefalleen duidelijke spraak

Belangrijk detail

WebRTC VAD accepteert alleen frames van:

10 ms
20 ms
30 ms

Dus:

framebytes
10 ms320
20 ms640
30 ms960

Daarom gebruiken we:

FRAME_BYTES = 640

Silero VAD gebruiken met whisper.cpp #

Model Description #

Silero VAD: pre-trained enterprise-grade Voice Activity Detector (VAD). Enterprise-grade Speech Products made refreshingly simple (see our STT models). Each model is published separately.

Currently, there are hardly any high quality / modern / free / public voice activity detectors except for WebRTC Voice Activity Detector (link). WebRTC though starts to show its age and it suffers from many false positives.

(!!!) Important Notice (!!!) – the models are intended to run on CPU only and were optimized for performance on 1 CPU thread. Note that the model is quantized.

Download: https://huggingface.co/ggml-org/whisper-vad/tree/main

Dus met VAD hoeft Whisper veel minder audio te verwerken.

Waarom dit zo nuttig is

Vooral bij audio met veel stilte:

situatiewinst
interviewsgroot
podcastsgroot
meetingsgroot
CCTV audioenorm
livestream captureenorm

Wil je echt een profi STT commando, gebruik deze:

whisper-cli.exe -m "model/ggml-large-v2-q5_0.bin" --vad -vm "model/ggml-silero-v5.1.2.bin" -f "test.wav" -sns -osrt

Voor server:

whisper-server.exe -m "model/ggml-large-v2-q5_0.bin" --vad --vad-model "model/ggml-silero-v5.1.2.bin" --port 9090

Verschil met WebRTC #

WebRTC kijkt naar audio features zoals:

  • energie
  • frequentiebanden
  • periodiciteit
  • ruisniveau

Dus ongeveer:

audio frame

feature extractie

regels / thresholds

speech / no speech

Voordelen:

✔ extreem snel
✔ bijna geen CPU
✔ stabiel

Nadelen:

❌ gevoelig voor ruis
❌ moeite met zachte stemmen
❌ kan muziek verwarren met spraak
❌ mist soms woorden aan begin/einde

Waarom dat 8 MB model zo klein is #

Omdat het alleen dit leert:

speech vs non speech

Dus geen taal, geen woorden, daarom kan het extreem klein blijven.

Performance vergelijking #

VADCPUaccuracyrealtime
WebRTC⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Silero⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐