Skip to content

Commit 68a6bdb

Browse files
committed
feat: python bindings
1 parent 7d7052b commit 68a6bdb

11 files changed

Lines changed: 292 additions & 1 deletion

File tree

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
name: Publish Python bindings
2+
on:
3+
workflow_dispatch:
4+
push:
5+
tags:
6+
- 'py-v*'
7+
env:
8+
MATURIN_MANIFEST: bindings/py/Cargo.toml
9+
jobs:
10+
linux:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
target: [x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu]
16+
steps:
17+
- uses: actions/checkout@v7
18+
- name: Build wheels
19+
uses: PyO3/maturin-action@v1
20+
with:
21+
target: ${{ matrix.target }}
22+
manylinux: auto
23+
args: --release --out dist -m ${{ env.MATURIN_MANIFEST }}
24+
- uses: actions/upload-artifact@v7
25+
with:
26+
name: wheels-linux-${{ matrix.target }}
27+
path: dist
28+
musllinux:
29+
runs-on: ubuntu-latest
30+
strategy:
31+
fail-fast: false
32+
matrix:
33+
target: [x86_64-unknown-linux-musl, aarch64-unknown-linux-musl]
34+
steps:
35+
- uses: actions/checkout@v7
36+
- name: Build wheels
37+
uses: PyO3/maturin-action@v1
38+
with:
39+
target: ${{ matrix.target }}
40+
manylinux: musllinux_1_2
41+
args: --release --out dist -m ${{ env.MATURIN_MANIFEST }}
42+
- uses: actions/upload-artifact@v7
43+
with:
44+
name: wheels-musllinux-${{ matrix.target }}
45+
path: dist
46+
windows:
47+
runs-on: windows-latest
48+
steps:
49+
- uses: actions/checkout@v7
50+
- name: Build wheels
51+
uses: PyO3/maturin-action@v1
52+
with:
53+
target: x86_64-pc-windows-msvc
54+
args: --release --out dist -m ${{ env.MATURIN_MANIFEST }}
55+
- uses: actions/upload-artifact@v7
56+
with:
57+
name: wheels-windows-x64
58+
path: dist
59+
macos:
60+
runs-on: macos-26
61+
steps:
62+
- uses: actions/checkout@v7
63+
- name: Build wheels
64+
uses: PyO3/maturin-action@v1
65+
with:
66+
target: aarch64-apple-darwin
67+
args: --release --out dist -m ${{ env.MATURIN_MANIFEST }}
68+
- uses: actions/upload-artifact@v7
69+
with:
70+
name: wheels-macos
71+
path: dist
72+
sdist:
73+
runs-on: ubuntu-slim
74+
steps:
75+
- uses: actions/checkout@v7
76+
- name: Build sdist
77+
uses: PyO3/maturin-action@v1
78+
with:
79+
command: sdist
80+
args: --out dist -m ${{ env.MATURIN_MANIFEST }}
81+
- uses: actions/upload-artifact@v7
82+
with:
83+
name: wheels-sdist
84+
path: dist
85+
publish:
86+
name: Publish to PyPI
87+
runs-on: ubuntu-slim
88+
needs: [linux, musllinux, windows, macos, sdist]
89+
environment:
90+
name: pypi
91+
url: https://pypi.org/project/earshot/
92+
permissions:
93+
id-token: write
94+
steps:
95+
- uses: actions/download-artifact@v7
96+
with:
97+
pattern: wheels-*
98+
path: dist
99+
merge-multiple: true
100+
- name: Publish
101+
uses: pypa/gh-action-pypi-publish@release/v1
102+
with:
103+
packages-dir: dist

.github/workflows/test-python.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Test Python bindings
2+
on:
3+
workflow_dispatch:
4+
push:
5+
branches:
6+
- 'main'
7+
paths:
8+
- '.github/workflows/test-python.yml'
9+
- 'src/**/*.rs'
10+
- 'bindings/py/**'
11+
- 'Cargo.toml'
12+
pull_request:
13+
paths:
14+
- '.github/workflows/test-python.yml'
15+
- 'src/**/*.rs'
16+
- 'bindings/py/**'
17+
- 'Cargo.toml'
18+
jobs:
19+
test:
20+
name: Test
21+
runs-on: ${{ matrix.platform.os }}
22+
strategy:
23+
fail-fast: false
24+
matrix:
25+
platform:
26+
- os: ubuntu-latest
27+
- os: windows-latest
28+
- os: macos-26
29+
steps:
30+
- uses: actions/checkout@v7
31+
- uses: dtolnay/rust-toolchain@stable
32+
- uses: Swatinem/rust-cache@v2
33+
with:
34+
workspaces: bindings/py
35+
- name: Test
36+
working-directory: bindings/py
37+
shell: bash
38+
run: |
39+
python -m venv .venv
40+
if [ -f .venv/bin/activate ]; then source .venv/bin/activate; else source .venv/Scripts/activate; fi
41+
pip install maturin
42+
maturin develop --release
43+
python -c "
44+
from earshot import Detector
45+
d = Detector()
46+
score = d.predict_i16([0] * 256)
47+
assert 0.0 <= score <= 1.0
48+
"

.gitignore

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
11
/target
2-
/Cargo.lock
2+
/Cargo.lock
3+
.venv/
4+
**/*.pyd
5+
**/*.pdb
6+
**/*.so
7+
**/*.dylib
8+
__pycache__/

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
[workspace]
2+
members = [
3+
"bindings/py"
4+
]
5+
16
[package]
27
name = "earshot"
38
version = "1.2.0"

bindings/py/Cargo.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "earshot-py"
3+
version = "1.2.0"
4+
edition = "2024"
5+
publish = false
6+
7+
[lib]
8+
name = "_earshot"
9+
crate-type = ["cdylib"]
10+
11+
[dependencies]
12+
earshot = { path = "../.." }
13+
pyo3 = { version = "0.29", features = [ "extension-module", "abi3-py38" ] }

bindings/py/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Earshot
2+
Ridiculously fast & accurate voice activity detection.

bindings/py/pyproject.toml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[build-system]
2+
requires = ["maturin>=1.7,<2.0"]
3+
build-backend = "maturin"
4+
5+
[project]
6+
name = "earshot"
7+
version = "1.2.0"
8+
description = "Ridiculously fast & accurate voice activity detection"
9+
readme = "README.md"
10+
license = "MIT OR Apache-2.0"
11+
requires-python = ">=3.8"
12+
authors = [{ name = "Carson M", email = "carson@pyke.io" }]
13+
keywords = ["vad", "voice-activity-detection", "audio", "speech"]
14+
classifiers = [
15+
"Programming Language :: Rust",
16+
"Topic :: Multimedia :: Sound/Audio :: Analysis",
17+
"Topic :: Scientific/Engineering :: Artificial Intelligence"
18+
]
19+
20+
[project.urls]
21+
Repository = "https://github.com/pykeio/earshot"
22+
Documentation = "https://github.com/pykeio/earshot/tree/main/bindings/py"
23+
24+
[tool.maturin]
25+
manifest-path = "Cargo.toml"
26+
module-name = "earshot._earshot"
27+
python-source = "python"
28+
features = ["pyo3/extension-module"]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from ._earshot import Detector, __version__
2+
3+
__all__ = ['Detector', '__version__']
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from typing import Sequence
2+
3+
__version__: str
4+
5+
class Detector:
6+
def __init__(self) -> None: ...
7+
8+
def reset(self) -> None:
9+
"""
10+
Resets the internal state of the voice activity detector.
11+
12+
The detector should be reset whenever:
13+
- the recording device changes; or
14+
- the detector is being used for a new audio sequence.
15+
"""
16+
...
17+
18+
def predict_i16(self, frame: Sequence[int]) -> float:
19+
"""
20+
Predicts the voice activity score of a single input frame of 16-bit PCM audio.
21+
22+
The frame:
23+
- should be sampled at 16 KHz;
24+
- should be exactly 256 samples (so 16 ms) in length.
25+
26+
The output score is between `[0, 1]`. Scores over 0.5 can generally be considered voice, but the exact threshold
27+
can be adjusted according to application-specific needs.
28+
"""
29+
...
30+
31+
def predict_f32(self, frame: Sequence[float]) -> float:
32+
"""
33+
Predicts the voice activity score of a single input frame of 32-bit floating-point PCM audio.
34+
35+
The frame:
36+
- should be sampled at 16 KHz;
37+
- should be exactly 256 samples (so 16 ms) in length;
38+
- should consist only of samples in the range [-1, 1].
39+
40+
The output score is between `[0, 1]`. Scores over 0.5 can generally be considered voice, but the exact threshold
41+
can be adjusted according to application-specific needs.
42+
"""
43+
...

bindings/py/python/earshot/py.typed

Whitespace-only changes.

0 commit comments

Comments
 (0)