Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions .github/workflows/metal-wheel.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: Apple Silicon Metal wheel

"on":
pull_request:
workflow_dispatch:
inputs:
qwentts_ref:
description: qwentts.cpp revision (must match the binding's verified ABI)
type: string
default: 7df559a8ca25f66fee02970514ebe5f01dee9055
workflow_call:
inputs:
qwentts_ref:
type: string
default: 7df559a8ca25f66fee02970514ebe5f01dee9055

permissions:
contents: read

jobs:
metal:
runs-on: macos-14
env:
MACOSX_DEPLOYMENT_TARGET: "14.0"
QWENTTS_REF: ${{ inputs.qwentts_ref || '7df559a8ca25f66fee02970514ebe5f01dee9055' }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install build and test tools
run: python -m pip install build delocate twine pytest numpy huggingface-hub
- name: Fetch pinned native source
run: |
git init third_party/qwentts.cpp
git -C third_party/qwentts.cpp remote add origin https://github.com/ServeurpersoCom/qwentts.cpp
git -C third_party/qwentts.cpp fetch --depth 1 origin "$QWENTTS_REF"
git -C third_party/qwentts.cpp checkout --detach FETCH_HEAD
git -C third_party/qwentts.cpp submodule update --init --recursive --depth 1
- name: Build and repair Metal wheel
run: |
test "$(uname -m)" = arm64
python scripts/set_local_version.py metal
python scripts/build_native.py --backend metal --clean
# setup-python may provide universal2 Python; the bundled dylibs are arm64 only.
python -m build --wheel --config-setting=--build-option=--plat-name=macosx_14_0_arm64
delocate-wheel --require-archs arm64 -w wheelhouse -v dist/*.whl
python -m twine check --strict wheelhouse/*.whl
- name: Check ctypes layouts against the pinned header
env:
PYTHONPATH: src
QWENTTS_CPP_SOURCE: third_party/qwentts.cpp
QWENTTS_CPP_LIBRARY: ${{ github.workspace }}/src/qwentts_cpp/lib/libqwen.dylib
run: python -m pytest -q tests
- name: Test installed wheel without the source or build tree
run: |
python -m venv "$RUNNER_TEMP/metal-wheel-test"
"$RUNNER_TEMP/metal-wheel-test/bin/python" -m pip install --only-binary=:all: wheelhouse/*.whl
mv build "$RUNNER_TEMP/native-build"
mv third_party "$RUNNER_TEMP/native-source"
mv src/qwentts_cpp/lib "$RUNNER_TEMP/package-libraries"
cd "$RUNNER_TEMP"
env -u QWENTTS_CPP_LIBRARY -u QWEN_LIBRARY_PATH -u DYLD_LIBRARY_PATH \
"$RUNNER_TEMP/metal-wheel-test/bin/python" -I -c \
"from qwentts_cpp import QwenLibrary; lib = QwenLibrary(); print(lib.path); print(lib.version())"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: hf-wheel-metal-macosx-arm64
path: wheelhouse/*.whl
18 changes: 14 additions & 4 deletions .github/workflows/publish-hf-wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ env:
QWENTTS_REF: ${{ inputs.qwentts_ref || '7df559a8ca25f66fee02970514ebe5f01dee9055' }}

jobs:
build-metal-wheel:
uses: ./.github/workflows/metal-wheel.yml
with:
qwentts_ref: ${{ inputs.qwentts_ref || '7df559a8ca25f66fee02970514ebe5f01dee9055' }}

build-wheels:
name: Linux ${{ matrix.arch }} CUDA ${{ matrix.cuda_version }} HF wheel
runs-on: ${{ matrix.runner }}
Expand Down Expand Up @@ -154,8 +159,10 @@ jobs:

- name: Fetch qwentts.cpp
run: |
git clone --recursive https://github.com/ServeurpersoCom/qwentts.cpp third_party/qwentts.cpp
git -C third_party/qwentts.cpp checkout "$QWENTTS_REF"
git init third_party/qwentts.cpp
git -C third_party/qwentts.cpp remote add origin https://github.com/ServeurpersoCom/qwentts.cpp
git -C third_party/qwentts.cpp fetch --depth 1 origin "$QWENTTS_REF"
git -C third_party/qwentts.cpp checkout --detach FETCH_HEAD
git -C third_party/qwentts.cpp submodule update --init --recursive

- name: Build wheel
Expand Down Expand Up @@ -222,8 +229,10 @@ jobs:

- name: Fetch qwentts.cpp
run: |
git clone --recursive https://github.com/ServeurpersoCom/qwentts.cpp third_party/qwentts.cpp
git -C third_party/qwentts.cpp checkout "$QWENTTS_REF"
git init third_party/qwentts.cpp
git -C third_party/qwentts.cpp remote add origin https://github.com/ServeurpersoCom/qwentts.cpp
git -C third_party/qwentts.cpp fetch --depth 1 origin "$QWENTTS_REF"
git -C third_party/qwentts.cpp checkout --detach FETCH_HEAD
git -C third_party/qwentts.cpp submodule update --init --recursive

- name: Build wheel
Expand Down Expand Up @@ -252,6 +261,7 @@ jobs:
needs:
- build-wheels
- build-cpu-wheels
- build-metal-wheel
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,10 @@ jobs:

- name: Fetch qwentts.cpp
run: |
git clone --recursive https://github.com/ServeurpersoCom/qwentts.cpp third_party/qwentts.cpp
git -C third_party/qwentts.cpp checkout "$QWENTTS_REF"
git init third_party/qwentts.cpp
git -C third_party/qwentts.cpp remote add origin https://github.com/ServeurpersoCom/qwentts.cpp
git -C third_party/qwentts.cpp fetch --depth 1 origin "$QWENTTS_REF"
git -C third_party/qwentts.cpp checkout --detach FETCH_HEAD
git -C third_party/qwentts.cpp submodule update --init --recursive

- name: Build wheel
Expand Down
17 changes: 13 additions & 4 deletions .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ name: qwentts-cpp-python wheels
default: 7df559a8ca25f66fee02970514ebe5f01dee9055

jobs:
macos-metal:
uses: ./.github/workflows/metal-wheel.yml
with:
qwentts_ref: ${{ inputs.qwentts_ref || '7df559a8ca25f66fee02970514ebe5f01dee9055' }}

linux-cuda:
name: Linux ${{ matrix.arch }} CUDA ${{ matrix.cuda_version }} wheel
runs-on: ${{ matrix.runner }}
Expand Down Expand Up @@ -121,8 +126,10 @@ jobs:

- name: Fetch qwentts.cpp
run: |
git clone --recursive https://github.com/ServeurpersoCom/qwentts.cpp third_party/qwentts.cpp
git -C third_party/qwentts.cpp checkout "$QWENTTS_REF"
git init third_party/qwentts.cpp
git -C third_party/qwentts.cpp remote add origin https://github.com/ServeurpersoCom/qwentts.cpp
git -C third_party/qwentts.cpp fetch --depth 1 origin "$QWENTTS_REF"
git -C third_party/qwentts.cpp checkout --detach FETCH_HEAD
git -C third_party/qwentts.cpp submodule update --init --recursive

- name: Build wheel
Expand Down Expand Up @@ -184,8 +191,10 @@ jobs:

- name: Fetch qwentts.cpp
run: |
git clone --recursive https://github.com/ServeurpersoCom/qwentts.cpp third_party/qwentts.cpp
git -C third_party/qwentts.cpp checkout "$QWENTTS_REF"
git init third_party/qwentts.cpp
git -C third_party/qwentts.cpp remote add origin https://github.com/ServeurpersoCom/qwentts.cpp
git -C third_party/qwentts.cpp fetch --depth 1 origin "$QWENTTS_REF"
git -C third_party/qwentts.cpp checkout --detach FETCH_HEAD
git -C third_party/qwentts.cpp submodule update --init --recursive

- name: Build wheel
Expand Down
14 changes: 11 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,17 @@ wheels from the pinned qwentts.cpp source.
Additional backend-specific wheels can be published to Hugging Face Hub by
manually dispatching `.github/workflows/publish-hf-wheels.yml`. That workflow
builds local-version variants such as `0.2.0+cpu`, `0.2.0+cu124`,
`0.2.0+cu128`, and `0.2.0+cu130`, prepares static `--find-links` pages,
`0.2.0+cu128`, `0.2.0+cu130`, and `0.3.1+metal`, prepares static `--find-links` pages,
creates the public dataset repo if needed, and uploads the wheel index using
the `HF_TOKEN` repository secret. Do not upload those local-version variants to
PyPI.

The `+metal` flavor is a macOS 14+ arm64 wheel with embedded Metal shaders and
bundled native dylibs. The reusable `.github/workflows/metal-wheel.yml` builds,
repairs, validates ABI layouts, and tests a clean wheel install. It runs for PRs
and is also called by validation and Hugging Face publishing. GPU synthesis is
tested locally with `scripts/smoke_stream.py --require-metal` and local weights.

Pull requests do not run the Linux wheel matrix. Do not run the validation
workflow solely as a publishing prerequisite, because the publishing workflows
perform fresh builds.
Expand All @@ -38,8 +44,10 @@ To rebuild against a newer qwentts.cpp revision:

1. Resolve the latest upstream `master` commit to its full SHA.
2. Update every default and event fallback for `QWENTTS_REF` in
`.github/workflows/wheels.yml`, `.github/workflows/publish.yml`, and
`.github/workflows/publish-hf-wheels.yml`.
`.github/workflows/wheels.yml`, `.github/workflows/publish.yml`,
`.github/workflows/publish-hf-wheels.yml`, and `.github/workflows/metal-wheel.yml`.
Update `QWENTTS_NATIVE_REVISION` in `src/qwentts_cpp/_binding.py` and verify
ctypes layouts with `QWENTTS_CPP_SOURCE=/path/to/source python -m pytest tests/test_native_abi.py`.
3. Update the pinned revision and its summary in `README.md`.
4. Open a focused pull request containing the pin and documentation changes.
5. After merging, use the normal publishing paths when a release is intended:
Expand Down
75 changes: 71 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@ python scripts/build_native.py \
QWENTTS_CPP_WHEEL_BUILD_TAG=1cpu python -m build --wheel
```

`--backend cuda` is the default because `faster-qwen3-tts` is a CUDA-first
package. CPU builds are still useful for development and smoke tests, but they
are not the primary release target.
`--backend metal` is the default on macOS; other platforms default to CUDA.
CPU builds are still useful for development and smoke tests.

## Installation

Expand Down Expand Up @@ -69,14 +68,82 @@ the same backend flavor. For example, the `cu128` page can host both
`manylinux_2_35` wheels for Ubuntu 22.04+ and `manylinux_2_39` wheels for
Ubuntu 24.04+. Pip selects the newest compatible wheel for the current machine.

Pull requests do not build the wheel matrix. The PyPI and Hugging Face
Pull requests do not build the Linux wheel matrix. The PyPI and Hugging Face
publishing workflows each rebuild fresh wheels from the pinned qwentts.cpp
revision; validation artifacts are not reused for publishing.

### Apple Silicon (Metal)

The Hugging Face publisher builds a `+metal` wheel for **macOS 14 or newer,
arm64 Python 3.10+**. After this change is merged and the **Publish Hugging Face
Wheels** workflow has completed, install it with:

```bash
python -m pip install --only-binary=qwentts-cpp-python "qwentts-cpp-python==0.3.1+metal" \
-f https://huggingface.co/datasets/andito/qwentts-cpp-python-wheels/tree/main/whl/metal
python -c "from qwentts_cpp import QwenLibrary; print(QwenLibrary().version())"
```

No local CMake build, Homebrew libraries, or explicit `library_path` is needed.
The wheel bundles libqwen and the ggml CPU, Metal, BLAS, and core libraries;
Metal shader source is embedded in the Metal library. Apple frameworks come
from macOS. The build applies a shader-only workaround for the pinned ggml's
invalid scalar-to-BF16-vector fill cast, restoring the source checkout afterward;
the C ABI is unchanged. Use a native arm64 Python, rather than an Intel Python
under Rosetta.
The `+metal` version identifies the backend on Hugging Face; it is never sent to
PyPI and cannot collide with the Linux CUDA or other backend variants.

Before publication, download the `hf-wheel-metal-macosx-arm64` artifact from
the PR's **Apple Silicon Metal wheel** check, unzip it, and install its `.whl`
with `python -m pip install /path/to/qwentts_cpp_python-0.3.1+metal-*.whl`.
This macOS check also runs for PRs; the Linux matrix remains dispatch-only.
The Hugging Face publisher rebuilds and checks its own Metal wheel.

Run a streaming smoke test using local GGUF weights (weights are not bundled):

```bash
python scripts/smoke_stream.py \
--talker /path/to/qwen-talker-1.7b-base-Q8_0.gguf \
--codec /path/to/qwen-tokenizer-12hz-Q8_0.gguf \
--ref-spk /path/to/reference.spk \
--require-metal --output metal-smoke.wav
```

For a CustomVoice talker, replace `--ref-spk` with `--speaker Vivian`; for a
VoiceDesign talker, use `--instruct "A calm, clear voice."`. `--require-metal`
forces the Metal device so the test fails if only CPU execution is available.
The script checks multiple nonempty, finite audio chunks and writes a mono
24 kHz PCM WAV. Listen to it with `afplay metal-smoke.wav`. Hosted macOS CI
checks packaging, ABI layouts, and installed loading; synthesis requires a Mac
with a working Metal device and local weights.

To build the same wheel locally with the pinned source checkout:

```bash
python -m pip install build delocate twine
export MACOSX_DEPLOYMENT_TARGET=14.0
python scripts/set_local_version.py metal # changes local version metadata
python scripts/build_native.py --backend metal --clean
python -m build --wheel --config-setting=--build-option=--plat-name=macosx_14_0_arm64
delocate-wheel --require-archs arm64 -w wheelhouse -v dist/*.whl
python -m twine check --strict wheelhouse/*.whl
```

### Native ABI compatibility

The CI wheel build defaults to qwentts.cpp
`7df559a8ca25f66fee02970514ebe5f01dee9055`, which retains ABI v2 and includes
the latest static-graph, streaming-decode, and widened voice-route changes.

The loader verifies this native revision before calling functions that write
ctypes parameter buffers. Upstream does not expose an ABI-version or struct-size
query, so other revisions (including unknown builds) are rejected with an
actionable error even if they may be compatible. When updating the pin, update
`QWENTTS_NATIVE_REVISION` in the binding and run `tests/test_native_abi.py` with
`QWENTTS_CPP_SOURCE` pointing to the new checkout to verify every struct size
and field offset. An incompatible library fails before model loading.

`QWENTTS_CPP_WHEEL_BUILD_TAG` is useful for local wheelhouses. For public
indexes, publish one backend flavor per package/version/platform compatibility
tag; otherwise pip has no way to choose between CPU and CUDA binaries.
Expand Down
8 changes: 7 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,16 @@ where = ["src"]
qwentts_cpp = ["py.typed", "lib/*"]

[tool.cibuildwheel]
build = "cp310-manylinux_*"
build = "cp310-manylinux_* cp310-macosx_arm64"
skip = "*-musllinux_* *-manylinux_i686"
before-build = "python scripts/build_native.py --backend cuda --clean"
test-command = "python -c \"from qwentts_cpp import QwenLibrary; print(QwenLibrary().version())\""

[tool.cibuildwheel.linux]
repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel}"

[tool.cibuildwheel.macos]
archs = ["arm64"]
environment = { MACOSX_DEPLOYMENT_TARGET = "14.0" }
before-build = "python scripts/build_native.py --backend metal --clean"
repair-wheel-command = "delocate-wheel --require-archs arm64 -w {dest_dir} -v {wheel}"
Loading
Loading