High-performance offline speech recognition library for Python, powered by C++ ffvoice-engine.
⚡ High Performance
- 3-10x faster than pure Python solutions
- C++ core with Python ease of use
- ~272MB memory footprint (vs 1-2GB for pure Python)
🔒 Privacy First
- 100% offline operation
- No data uploaded to cloud
- GDPR/HIPAA compliant
🎙️ Complete Audio Pipeline
- Real-time audio capture
- AI-powered noise reduction (RNNoise)
- Voice Activity Detection (VAD)
- Offline speech recognition (Whisper)
- Intelligent audio segmentation
🛠️ Easy to Use
- Simple Python API
- One-line installation:
pip install ffvoice - Comprehensive examples
| Platform | PyPI Wheel | Installation Method | Status |
|---|---|---|---|
| 🍎 Apple Silicon (M1/M2/M3) | ✅ ARM64 | pip install ffvoice |
✅ Fully Supported |
| 🍎 Intel Mac | ❌ Not Compatible | Build from source | |
| 🐧 Linux x86_64 | ✅ x86_64 | pip install ffvoice |
✅ Fully Supported |
| 🪟 Windows x86_64 | ✅ x86_64 | pip install ffvoice |
✅ Fully Supported |
Apple Silicon (M1/M2/M3), Linux x86_64 & Windows x86_64:
pip install ffvoiceApple Silicon Users 🍎
- ✅ One-line install works out of the box
- ✅ Native ARM64 performance
- ✅ No Rosetta 2 required
- Important: Make sure Python is running in ARM64 mode:
python -c "import platform; print(platform.machine())" # Should output: arm64
Intel Mac Users 🍎
⚠️ PyPI wheel is not compatible with Intel Macs- Solution: Build from source (see instructions below)
- The ARM64 wheel cannot be used even with Rosetta 2 translation
Rosetta 2 Users
- If you see
ImportError: mach-o file, but is an incompatible architecture, you're running x86_64 Python - Fix: Use native ARM64 Python instead:
# Force ARM64 architecture arch -arm64 python3 -m pip install ffvoice # Or permanently switch to ARM64 Python arch -arm64 /bin/bash python3 -m pip install ffvoice
Linux Users 🐧
- ✅ Direct install from PyPI works
- Tested on Ubuntu 20.04+, Debian 11+
- Requires system libraries (see Prerequisites below)
Windows Users 🪟
- ✅ One-line install works with prebuilt x86_64 wheels
- ✅ Supports Python 3.9-3.12
- ✅ Bundles required dependencies (no manual FFmpeg install needed)
- Note: RNNoise noise reduction is disabled on Windows (MSVC lacks VLA support); all other features work
System Dependencies:
macOS:
brew install ffmpeg portaudio flac cmakeLinux (Ubuntu/Debian):
sudo apt-get install libavcodec-dev libavformat-dev libavutil-dev \
libswresample-dev portaudio19-dev libflac-dev cmakePython Requirements:
- Python 3.7 or later
- pip
# Clone repository
git clone https://github.com/chicogong/ffvoice-engine.git
cd ffvoice-engine
# Install Python package
pip install .
# Or install in development mode
pip install -e .The installation automatically enables:
- RNNoise noise suppression
- Whisper ASR
To customize the build:
# Set environment variables before pip install
export CMAKE_ARGS="-DENABLE_RNNOISE=ON -DENABLE_WHISPER=ON"
pip install .import ffvoice
# Configure Whisper ASR
config = ffvoice.WhisperConfig()
config.model_type = ffvoice.WhisperModelType.TINY
config.language = "auto" # Auto-detect language
# Initialize ASR
asr = ffvoice.WhisperASR(config)
asr.initialize()
# Transcribe audio file
segments = asr.transcribe_file("audio.wav")
# Print results
for segment in segments:
print(f"[{segment.start_ms}ms -> {segment.end_ms}ms]")
print(f" {segment.text} (confidence: {segment.confidence:.2f})")import ffvoice
import numpy as np
# Load audio as NumPy array
audio = np.zeros(48000, dtype=np.int16) # 1 second at 48kHz
# Transcribe from NumPy array
config = ffvoice.WhisperConfig()
config.model_type = ffvoice.WhisperModelType.TINY
asr = ffvoice.WhisperASR(config)
asr.initialize()
segments = asr.transcribe_buffer(audio)
for segment in segments:
print(segment.text)import ffvoice
import numpy as np
# Initialize audio capture
ffvoice.AudioCapture.initialize()
# List available devices
devices = ffvoice.AudioCapture.get_devices()
for device in devices:
print(f"{device.id}: {device.name} (channels: {device.max_input_channels})")
# Create capture instance
capture = ffvoice.AudioCapture()
capture.open(sample_rate=48000, channels=1, frames_per_buffer=256)
# Define callback to process audio
def audio_callback(audio_array):
"""Receives NumPy array with audio samples"""
print(f"Received {len(audio_array)} samples")
# Process audio here...
# Start capture with callback
capture.start(audio_callback)
# ... capture runs in background ...
# Stop capture
capture.stop()
capture.close()
ffvoice.AudioCapture.terminate()import ffvoice
import numpy as np
# Configure RNNoise
config = ffvoice.RNNoiseConfig()
config.enable_vad = True
# Initialize noise reduction
rnnoise = ffvoice.RNNoise(config)
rnnoise.initialize(sample_rate=48000, channels=1)
# Process audio from NumPy array (in-place modification)
audio = np.random.randint(-1000, 1000, 256, dtype=np.int16)
rnnoise.process(audio) # Audio array is modified in-place
# Get VAD probability
vad_prob = rnnoise.get_vad_probability()
print(f"Voice activity: {vad_prob:.2%}")import ffvoice
import numpy as np
# Create VAD config with preset
config = ffvoice.VADConfig.from_preset(ffvoice.VADSensitivity.BALANCED)
# Initialize VAD segmenter — constructor takes only config, no sample_rate arg
vad = ffvoice.VADSegmenter(config)
# Define callback for complete segments
def segment_callback(segment_array):
"""Called when a complete speech segment is detected"""
print(f"Speech segment: {len(segment_array)} samples")
# Process or save the segment...
# Process audio frames with VAD
audio_frame = np.zeros(256, dtype=np.int16)
vad_prob = 0.8 # From RNNoise
vad.process_frame(audio_frame, vad_prob, segment_callback)
# Flush remaining audio at the end
vad.flush(segment_callback)
# get_statistics() returns a tuple (avg_vad_prob, speech_ratio), NOT a dict
avg_vad_prob, speech_ratio = vad.get_statistics()
print(f"Average VAD: {avg_vad_prob:.2f}")
print(f"Speech ratio: {speech_ratio:.2%}")
print(f"Is in speech: {vad.is_in_speech()}")import ffvoice
import numpy as np
# Create audio data
sample_rate = 48000
audio = np.random.randint(-1000, 1000, 48000, dtype=np.int16)
# Write WAV file
wav_writer = ffvoice.WAVWriter()
wav_writer.open("output.wav", sample_rate, channels=1)
samples_written = wav_writer.write_samples_array(audio)
wav_writer.close()
print(f"Wrote {samples_written} samples to WAV")
# Write FLAC file (with compression)
flac_writer = ffvoice.FLACWriter()
flac_writer.open("output.flac", sample_rate, channels=1, bits_per_sample=16, compression_level=5)
samples_written = flac_writer.write_samples_array(audio)
compression_ratio = flac_writer.get_compression_ratio()
flac_writer.close()
print(f"Wrote {samples_written} samples to FLAC (compression: {compression_ratio:.2f}x)")import ffvoice
# Enable per-word timestamps in the Whisper config
config = ffvoice.WhisperConfig()
config.model_type = ffvoice.WhisperModelType.TINY
config.word_timestamps = True # Populate `words` for every segment
asr = ffvoice.WhisperASR(config)
asr.initialize()
segments = asr.transcribe_file("audio.wav")
for segment in segments:
print(f"[{segment.start_ms}ms -> {segment.end_ms}ms] {segment.text}")
# Each segment now carries a list of `Word` objects
for word in segment.words:
print(f" {word.start_ms}-{word.end_ms}ms "
f"'{word.text}' (p={word.probability:.2f})")When transcribing a NumPy buffer whose sample rate is not 48000 Hz, set
config.input_sample_rate so the audio is resampled correctly:
config = ffvoice.WhisperConfig()
config.input_sample_rate = 16000 # Sample rate of the buffer passed to transcribe_buffer()import ffvoice
import numpy as np
# Create and initialize a mixer (channels must be 1 or 2)
mixer = ffvoice.AudioMixer()
mixer.initialize(sample_rate=48000, channels=2)
# Add tracks; add_track() returns the new track id
voice = mixer.add_track(gain=1.0, pan=0.0) # centered
music = mixer.add_track(gain=0.5, pan=-0.3) # quieter, slightly left
# Adjust tracks at any time
mixer.set_gain(music, 0.4)
mixer.set_pan(voice, 0.2)
mixer.set_mute(music, False)
mixer.set_master_gain(0.9)
# Mix a block: {track_id: int16 ndarray}; all arrays must be the same length
voice_block = np.random.randint(-2000, 2000, 480, dtype=np.int16)
music_block = np.random.randint(-2000, 2000, 480, dtype=np.int16)
mixed = mixer.mix_block({voice: voice_block, music: music_block})
print(f"Mixed {len(mixed)} samples across {mixer.get_track_count()} tracks")import ffvoice
import numpy as np
# SPSC ring buffer holding up to `capacity` int16 samples
buf = ffvoice.RingBuffer(capacity=4096)
# Bulk transfer with NumPy (e.g. audio thread -> processing thread)
samples = np.random.randint(-1000, 1000, 1024, dtype=np.int16)
pushed = buf.push_bulk(samples) # number actually pushed
print(f"pushed={pushed}, size={buf.size()}, full={buf.full()}")
chunk = buf.pop_bulk(512) # 1-D int16 ndarray (may be shorter)
print(f"popped {len(chunk)} samples, remaining={buf.size()}")
# Single-element access
buf.push(42)
value = buf.pop() # int, or None if empty
buf.clear()Offline speech recognition using Whisper models.
Methods:
initialize()- Load Whisper modeltranscribe_file(filename)- Transcribe audio file, returns list ofTranscriptionSegmenttranscribe_buffer(audio_array)- Transcribe from NumPy array (int16, 1D), returns list ofTranscriptionSegmentget_last_error()- Get last error message (string)get_last_inference_time_ms()- Get inference time in milliseconds (int)is_initialized()- Check if model is loaded (bool)
Real-time audio capture from microphone with callback support.
Methods:
open(device_id=-1, sample_rate=48000, channels=1, frames_per_buffer=256)- Open audio device. Note: the parameter isdevice_id(notdevice_index).start(callback)- Start capture with Python callback receiving NumPy arrays (int16, 1D)stop()- Stop audio captureclose()- Close audio deviceis_open()- Check if device is open (bool)is_capturing()- Check if currently capturing (bool)get_sample_rate()- Get sample rate (int)get_channels()- Get number of channels (int)
Static Methods:
initialize()- Initialize PortAudio systemterminate()- Terminate PortAudio systemget_devices()- Get list ofAudioDeviceInfoobjectsget_default_input_device()- Get default input device ID (int)
AI-powered noise reduction with NumPy support.
Methods:
initialize(sample_rate, channels)- Initialize processorprocess(audio_array)- Process NumPy array in-place (modifies input)reset()- Reset internal stateget_vad_probability()- Get VAD probability 0.0-1.0 (float)
Voice activity detection and intelligent segmentation with callbacks.
Constructor: VADSegmenter(config) — takes only a VADConfig; there is no sample_rate argument.
Methods:
process_frame(audio_array, vad_prob, callback)- Process audio frame with callback for complete segmentsflush(callback)- Flush remaining audio with callbackreset()- Reset segmenter stateis_in_speech()- Check if currently in speech (bool)get_buffer_size()- Get current buffer size in samples (int)get_current_threshold()- Get adaptive threshold (float)get_statistics()- Get VAD statistics as a tuple(avg_vad_prob, speech_ratio); unpack withavg_vad_prob, speech_ratio = vad.get_statistics()— it is not a dict.
Write audio to WAV files with NumPy support.
Methods:
open(filename, sample_rate, channels, bits_per_sample=16)- Open WAV file for writingwrite_samples_array(audio_array)- Write NumPy array (int16, 1D) to file, returns samples written (int)close()- Close file and finalize headersis_open()- Check if file is open (bool)get_total_samples()- Get total samples written (int)
Write audio to FLAC files with compression and NumPy support.
Methods:
open(filename, sample_rate, channels, bits_per_sample=16, compression_level=5)- Open FLAC filecompression_level: 0 (fastest) to 8 (best compression), default 5
write_samples_array(audio_array)- Write NumPy array (int16, 1D) to file, returns samples written (int)close()- Close file and finalizeget_compression_ratio()- Get compression ratio (float)is_open()- Check if file is open (bool)get_total_samples()- Get total samples written (int)
Multi-track mixer that combines several int16 audio tracks into one output.
Methods:
initialize(sample_rate, channels)- Initialize the mixer (channels must be 1 or 2)is_initialized()- Check if the mixer is initialized (bool)get_sample_rate()/get_channels()- Get the mixer configuration (int)add_track(gain=1.0, pan=0.0)- Add a track, returns its track id (int; -1 if not initialized)remove_track(track_id)- Remove a track, returnsTrueif it existed (bool)has_track(track_id)- Check whether a track exists (bool)get_track_count()- Number of registered tracks (int)set_gain(track_id, gain)/get_gain(track_id)- Track linear gain (clamped to [0, 8])set_pan(track_id, pan)/get_pan(track_id)- Track stereo pan (-1 = left, 0 = center, +1 = right)set_mute(track_id, muted)/is_muted(track_id)- Mute or unmute a trackset_master_gain(gain)/get_master_gain()- Master gain applied to the mix (clamped to [0, 8])mix_block(tracks)- Mix{track_id: int16 ndarray}into a single mixed int16 ndarray; all arrays must have the same lengthreset()- Remove all tracks and reset the master gain
Lock-free single-producer/single-consumer (SPSC) ring buffer for int16 audio samples.
Constructor:
RingBuffer(capacity)- Create a buffer holding up tocapacityint16 samples
Methods:
push(value)- Push one value, returnsFalseif the buffer is full (bool)pop()- Pop one value, returnsNoneif the buffer is empty (int or None)push_bulk(data)- Push values from a 1-D int16 ndarray, returns the number actually pushed (int)pop_bulk(count)- Pop up tocountvalues, returns them as a 1-D int16 ndarraysize()/capacity()- Current element count and maximum capacity (int)empty()/full()- Check buffer state (bool)available_read()- Number of elements available to pop (int)available_write()- Number of elements that can still be pushed (int)clear()- Reset the buffer to empty
Speech recognition result with timestamp.
Fields:
start_ms- Start time in milliseconds (int)end_ms- End time in milliseconds (int)text- Transcribed text (string)confidence- Confidence score 0.0-1.0 (float)words- List ofWordobjects (empty unlessWhisperConfig.word_timestampswas enabled)
Per-word timestamp within a TranscriptionSegment.
Fields:
start_ms- Word start time in milliseconds (int)end_ms- Word end time in milliseconds (int)text- Word text (string)probability- Mean token probability for this word, 0.0-1.0 (float)
Audio device information.
Fields:
id- Device ID (int)name- Device name (string)max_input_channels- Maximum input channels (int)max_output_channels- Maximum output channels (int)supported_sample_rates- List of supported sample rates (list of int)is_default- Is default device (bool)
model_path- Path to Whisper model filelanguage- Language code ('en', 'zh', 'auto')model_type- Model size (TINY, BASE, SMALL, MEDIUM, LARGE)n_threads- Number of CPU threadsenable_performance_metrics- Enable timing metricsword_timestamps- Populate per-word timestamps in eachTranscriptionSegment.words(bool, defaultFalse)input_sample_rate- Sample rate (Hz) of audio passed totranscribe_buffer()(int, default 48000)
There is no separate AudioCaptureConfig class. Pass these arguments directly to AudioCapture.open():
device_id- Audio device ID (-1 for system default)sample_rate- Sample rate in Hz (default: 48000)channels- Number of channels (default: 1)frames_per_buffer- Buffer size in frames (default: 256)
speech_threshold- VAD probability thresholdmin_speech_frames- Min frames to start speechmin_silence_frames- Min frames to end speechenable_adaptive_threshold- Enable adaptive adjustmentfrom_preset(sensitivity)(static) - Create from preset
TINY- Fastest (~39MB, ~10x realtime)BASE- Balanced (~74MB, ~7x realtime)SMALL- Better accuracy (~244MB, ~3x realtime)MEDIUM- High accuracy (~769MB, ~1x realtime)LARGE- Best accuracy (~1550MB, <1x realtime)
VERY_SENSITIVE- Detect very quiet speech (threshold=0.3)SENSITIVE- Detect quiet speech (threshold=0.4)BALANCED- Balanced detection (threshold=0.5)CONSERVATIVE- Avoid false positives (threshold=0.6)VERY_CONSERVATIVE- Only clear speech (threshold=0.7)
See the examples directory for complete working examples:
basic_transcription.py- Simple file transcriptionrealtime_transcription.py- Real-time audio processingcomplete_realtime_pipeline.py- End-to-end capture, denoise, VAD and recognition pipeline
Benchmark on Apple M1 Pro (8 cores):
| Model | Speed (RTF) | Memory | Use Case |
|---|---|---|---|
| TINY | ~10x | ~272MB | Real-time, fastest |
| BASE | ~7x | ~345MB | Balanced |
| SMALL | ~3x | ~512MB | Better accuracy |
| MEDIUM | ~1x | ~1.2GB | High quality |
| LARGE | <1x | ~2.1GB | Best quality |
RTF = Real-time Factor (higher is faster)
If you get ImportError: Failed to import ffvoice native module:
- Ensure all system dependencies are installed
- Rebuild the package:
pip install --force-reinstall . - Check CMake build logs for errors
If audio capture fails:
- List available devices:
ffvoice.AudioCapture.get_devices() - Check device permissions (microphone access)
- Try a different
device_idinAudioCapture.open()
If Whisper model fails to load:
- Check model file exists at the specified path
- Ensure you have enough disk space
- Verify model file is not corrupted
# Install dev dependencies
pip install -e .[dev]
# Run tests
pytest python/tests -v# Format code
black python/
# Check style
flake8 python/
# Type checking
mypy python/Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
MIT License - see LICENSE for details.