Skip to content

Add production bindings for Python, Android, and iOS - #5

Open
praveenperera wants to merge 17 commits into
masterfrom
ffi
Open

Add production bindings for Python, Android, and iOS#5
praveenperera wants to merge 17 commits into
masterfrom
ffi

Conversation

@praveenperera

@praveenperera praveenperera commented Jul 8, 2026

Copy link
Copy Markdown
Member

Implements SDK bindings across Python, Android, and iOS, with shared Rust SDK/FFI layers, explicit model preparation, typed configs/results/errors, Rust-owned audio decode/resample, queue support, demos, docs, and packaging workflows.

Includes local verification for Python CPU wheels, Android lean and bundled AARs with device smoke, Swift XCFramework/sample builds, and workspace Rust gates. GPU packages, hosted CI artifacts, registry signing/publish steps, hosted Swift binary URL, and iOS CoreML runtime smoke remain documented release blockers requiring external infrastructure or assets.

Summary by CodeRabbit

  • New Features
    • Added Python bindings with model preparation, diarization, queue handling, progress reporting, and a new CLI.
    • Added Android and Swift bindings, including Android and iOS diarization sample apps.
    • Introduced optional GPU Python distributions (CUDA and MIGraphX) with hardware smoke-test gating.
  • Bug Fixes
    • Improved CoreML handling across Apple platforms, including clearer missing-asset/runtime behavior.
    • Enhanced queued diarization results to include audio duration.
  • Documentation
    • Added end-to-end binding release checklists, platform guides, and API documentation.
  • Tests
    • Added/expanded Python end-to-end tests covering progress events, cancellation, queue flows, and CLI behavior.

Add the shared SDK, Python package crate, UniFFI crate, and core runtime support needed by the bindings plan.
Add the Gradle Android library, lean and bundled ORT flavors, sample app, wrapper files, and generated-output ignore rules.
Add the Swift package, generated UniFFI Swift source, XCFramework packaging scripts, and iOS sample app.
Add package workflow jobs, release docs, binding overview docs, and GPU Python distribution metadata.
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 83347878-6d26-4962-b5e9-d0ca61d1b3f5

📥 Commits

Reviewing files that changed from the base of the PR and between 9011a9c and c2eb844.

📒 Files selected for processing (6)
  • .github/workflows/ci.yml
  • .github/workflows/package.yml
  • crates/speakrs-python/pyproject.toml
  • crates/speakrs-python/python/speakrs/__init__.py
  • crates/speakrs-python/src/lib.rs
  • crates/speakrs-python/tests/test_python_api.py
✅ Files skipped from review due to trivial changes (1)
  • .github/workflows/ci.yml
🚧 Files skipped from review as they are similar to previous changes (5)
  • crates/speakrs-python/tests/test_python_api.py
  • .github/workflows/package.yml
  • crates/speakrs-python/pyproject.toml
  • crates/speakrs-python/python/speakrs/init.py
  • crates/speakrs-python/src/lib.rs

📝 Walkthrough

Walkthrough

This PR extracts shared SDK crates, adds Android/Swift/Python binding packages and samples, expands packaging automation, and updates core inference for iOS CoreML support and a pure nalgebra backend.

Changes

Core SDK substrate

Layer / File(s) Summary
SDK DTOs and error types
crates/speakrs-sdk/src/dto.rs, crates/speakrs-sdk/src/cancel.rs, crates/speakrs-sdk/src/error.rs, crates/speakrs-sdk/src/lib.rs
Defines shared execution-mode, config, result, progress, cancellation, and stable SDK error types and re-exports them from the SDK crate root.
Model management and audio decoding
crates/speakrs-sdk/Cargo.toml, crates/speakrs-sdk/src/models.rs, crates/speakrs-sdk/src/audio.rs
Adds model manifest/cache management, required artifact lists, and audio decode/resample helpers used to normalize inputs.
SDK facade pipeline and queue
crates/speakrs-sdk/src/facade.rs, src/pipeline/queued.rs
Wraps diarization, queueing, cancellation, timing, and duration propagation around the core pipeline.
Optional ORT sessions and CoreML/iOS support
src/inference.rs, src/inference/embedding*, src/inference/segmentation*, src/lib.rs, Cargo.toml
Makes embedding and segmentation sessions optional for native CoreML paths, extends CoreML support to iOS, and updates Android ORT resolution.
Pluggable linear algebra backend and PLDA update
src/linalg.rs, src/clustering/plda.rs
Introduces the local linalg error/backend abstraction and updates PLDA to use shared inverse/eigendecomposition helpers.

Sequence Diagram(s)

sequenceDiagram
  participant PythonAPI
  participant SdkPipeline
  participant ModelStore
  participant AudioDecode
  participant Queue
  PythonAPI->>SdkPipeline: prepare / from_prepared / diarize_file
  SdkPipeline->>ModelStore: load or verify prepared models
  SdkPipeline->>AudioDecode: decode and resample audio
  SdkPipeline->>SdkPipeline: run diarization
  SdkPipeline->>Queue: push or receive queued jobs
Loading

Public bindings, packaging, and release workflow

Layer / File(s) Summary
Android Gradle library and Rust cross-compilation
bindings/android/*
Adds the Android Gradle library module, Rust cross-compilation, UniFFI Kotlin generation, and published bundledOrt/lean artifacts.
Android sample app
bindings/android/sample/*
Adds the Android sample app that copies a bundled WAV fixture, prepares models, and runs diarization from MainActivity.
Swift package and UniFFI bindings
bindings/swift/*
Adds the SwiftPM manifest, generated Swift FFI bindings, XCFramework build/package scripts, and the public Swift wrapper types.
Swift DiarizeFile sample app
bindings/swift/Samples/DiarizeFile/*
Adds the iOS sample project, SwiftUI app, and project wiring for bundled audio and local Speakrs package use.
Python package metadata and native extension bindings
bindings/python/*, crates/speakrs-python/*
Adds the Python distribution metadata, package README files, the PyO3 native extension, CLI entry point, and Python API tests for CPU, CUDA, and MIGraphX builds.
Release documentation
bindings/README.md, bindings/RELEASE.md
Documents the binding artifact matrix, release checklist, registry ownership, signing, and publish blockers.
Packaging CI workflow jobs
.github/workflows/package.yml, .github/workflows/ci.yml, .gitattributes
Expands CI with manual GPU dispatch, Rust docs, wheel builds, Android packaging checks, Swift XCFramework packaging, conditional GPU wheel jobs, and Windows wrapper line-ending handling.

Estimated code review effort: 5 (Critical) | ~150 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding production bindings for Python, Android, and iOS.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ffi

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds production SDK/FFI layers shared by the Python, Android, and iOS bindings, alongside a new pure-linalg backend (nalgebra) for platforms without BLAS, CoreML iOS support, and a Rust-owned audio decode/resample pipeline. Packaging workflows for Python wheels, Android AARs, and a Swift XCFramework are also introduced.

  • New crates: speakrs-sdk (shared facade, audio, models, DTOs), speakrs-ffi (UniFFI for Swift/Android), speakrs-python (PyO3); all three wrap the same SdkPipeline / SdkQueue logic with typed configs, results, and errors.
  • Core changes: primary ORT sessions are now Option<Session> so CoreML-native mode can skip ONNX loading; linalg.rs gains a pure-linalg feature backed by nalgebra; QueuedDiarizationJobId exposes a stable as_u64() accessor.
  • CI: cibuildwheel jobs for Linux/macOS Python wheels, Gradle Android AAR/sample builds, and xcodebuild Swift XCFramework/sample steps are wired into package.yml.

Confidence Score: 3/5

The pipeline and file-diarization paths are well-tested and structurally sound; the queue path has a fundamental concurrency bug that will deadlock in the intended multi-threaded usage across all three binding layers.

Both the FFI and Python queue wrappers hold a single Mutex across the entire blocking recv() call. The underlying queue is designed for concurrent producer/consumer use, but the mutex prevents a producer thread from pushing while a consumer thread is waiting for results. This affects Swift/Android and Python bindings equally and would surface immediately in any non-trivial integration test.

crates/speakrs-ffi/src/lib.rs and crates/speakrs-python/src/lib.rs both need the Mutex replaced by independently-locked sender/receiver fields. crates/speakrs-sdk/src/facade.rs warrants a follow-up for the hard-coded duration: 0.0 in queue results.

Important Files Changed

Filename Overview
crates/speakrs-ffi/src/lib.rs UniFFI bindings for Swift/Android; SpeakrsQueue::recv holds Mutex while blocking, causing deadlock with concurrent pushes
crates/speakrs-python/src/lib.rs PyO3 Python bindings; same queue deadlock pattern as FFI — recv holds Mutex while blocking
crates/speakrs-sdk/src/facade.rs SDK facade over the diarization pipeline and queue; queue results always report duration=0.0
crates/speakrs-sdk/src/audio.rs Audio decode/resample using Symphonia and Rubato; well-tested, handles stereo downmix and sample-rate conversion to 16 kHz
crates/speakrs-sdk/src/models.rs Model store: manifest generation, SHA-256 verification, optional HuggingFace download, cache list/cleanup; comprehensive tests
src/linalg.rs Adds pure-linalg backend via nalgebra (inverse + generalized eigendecomposition), refactors existing BLAS backends into sub-modules
src/inference/embedding/load/sessions.rs Makes primary ORT sessions optional for CoreML-native mode; skips ONNX session loading when use_native_coreml is true
src/inference/segmentation.rs Primary segmentation session is now Option, skipped when CoreML native is active; has_batched_session helper added
.github/workflows/package.yml Adds CI jobs for Python wheels (cibuildwheel), Android AARs, and Swift XCFramework; GPU jobs conditional on workflow dispatch input

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    PY[Python caller] -->|PyO3| PYB[speakrs-python]
    SW[Swift / Kotlin caller] -->|UniFFI| FFI[speakrs-ffi]
    PYB --> SDK[speakrs-sdk facade]
    FFI --> SDK
    SDK --> PIPELINE[SdkPipeline\nrun_with_config]
    SDK --> QUEUE[SdkQueue\npush / recv]
    PIPELINE --> SPEAKRS[speakrs core]
    QUEUE --> SPEAKRS
    SDK --> AUDIO[audio.rs\ndecode + resample]
    SDK --> MODELS[models.rs\nModelStore + manifest verify]
    MODELS -->|optional| HF[HuggingFace\nhf-hub]
    SPEAKRS --> LINALG{linalg backend}
    LINALG -->|intel-mkl| MKL[Intel MKL]
    LINALG -->|openblas-*| OB[OpenBLAS]
    LINALG -->|pure-linalg| NA[nalgebra]
    SPEAKRS -->|coreml feature| COREML[CoreML native\nno ORT sessions]
    SPEAKRS -->|default| ORT[ONNX Runtime\nSessions]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    PY[Python caller] -->|PyO3| PYB[speakrs-python]
    SW[Swift / Kotlin caller] -->|UniFFI| FFI[speakrs-ffi]
    PYB --> SDK[speakrs-sdk facade]
    FFI --> SDK
    SDK --> PIPELINE[SdkPipeline\nrun_with_config]
    SDK --> QUEUE[SdkQueue\npush / recv]
    PIPELINE --> SPEAKRS[speakrs core]
    QUEUE --> SPEAKRS
    SDK --> AUDIO[audio.rs\ndecode + resample]
    SDK --> MODELS[models.rs\nModelStore + manifest verify]
    MODELS -->|optional| HF[HuggingFace\nhf-hub]
    SPEAKRS --> LINALG{linalg backend}
    LINALG -->|intel-mkl| MKL[Intel MKL]
    LINALG -->|openblas-*| OB[OpenBLAS]
    LINALG -->|pure-linalg| NA[nalgebra]
    SPEAKRS -->|coreml feature| COREML[CoreML native\nno ORT sessions]
    SPEAKRS -->|default| ORT[ONNX Runtime\nSessions]
Loading

Reviews (1): Last reviewed commit: "Add binding release packaging" | Re-trigger Greptile

Comment thread crates/speakrs-ffi/src/lib.rs
Comment thread crates/speakrs-python/src/lib.rs Outdated
Comment thread crates/speakrs-sdk/src/facade.rs
Protect queue receiver state with a mutex so queued jobs can be
received across threads while producers continue to enqueue.
Propagate segment duration through the queue result so bindings can
return timing metadata.
Add Python and SDK tests to verify concurrent receive behavior and
non-zero duration in queue results.
Extend package metadata and CI to target CPython 3.14,
including wheel matrix selectors and package classifiers.
Update the release checklist and Rust ORT crate feature set to keep
build behavior consistent with the new packaging configuration.,

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (8)
.github/workflows/package.yml (3)

104-128: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Set persist-credentials: false on checkout steps.

The actions/checkout@v6 action defaults to persist-credentials: true, which leaves the GitHub token in the local git config. For jobs that produce release artifacts, this is an unnecessary credential exposure surface. Consider adding persist-credentials: false to all checkout steps in this workflow.

🔒 Apply across all checkout steps
       - uses: actions/checkout@v6
+        with:
+          persist-credentials: false
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/package.yml around lines 104 - 128, Update the workflow’s
`actions/checkout@v6` step in the `rust-api-docs` job to set
`persist-credentials: false`, since checkout currently leaves the GitHub token
in local git config. Apply the same setting to any other `actions/checkout`
steps in this workflow so release-artifact jobs do not retain credentials
unnecessarily.

Source: Linters/SAST tools


342-393: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting CUDA/MIGraphX jobs into a reusable workflow.

The python-cuda-wheel (lines 289-341) and python-migraphx-wheel (lines 342-393) jobs are near-identical, differing only in execution mode name, runner label, cache directory, and output path. A matrix-based approach or reusable workflow would eliminate this duplication and reduce the risk of the two jobs drifting apart over time.

♻️ Example matrix approach
  python-gpu-wheel:
    name: Python ${{ matrix.name }} wheel smoke
    if: ${{ github.event_name == 'workflow_dispatch' && (inputs.gpu_python_package == matrix.name || inputs.gpu_python_package == 'both') }}
    runs-on: [self-hosted, linux, x64, ${{ matrix.runner_label }}]
    strategy:
      matrix:
        include:
          - name: cuda
            runner_label: cuda
            mode: CUDA
            cache_dir: gpu-cuda-cache
          - name: migraphx
            runner_label: migraphx
            mode: MIGraphX
            cache_dir: gpu-migraphx-cache
    steps:
      - uses: actions/checkout@v6
        with:
          persist-credentials: false
      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
      - name: Install Python
        uses: actions/setup-python@v6
        with:
          python-version: "3.12"
      - name: Install Python build tools
        run: python -m pip install --upgrade pip uv
      - name: Build ${{ matrix.name }} wheel
        working-directory: bindings/python/speakrs-${{ matrix.name }}
        run: uvx maturin build --release -i python3.12 --compatibility linux --out ../../../wheelhouse/speakrs-${{ matrix.name }}
      - name: Install ${{ matrix.name }} wheel
        shell: bash
        run: |
          set -euo pipefail
          python -m pip install --upgrade pip
          python -m pip install wheelhouse/speakrs-${{ matrix.name }}/*.whl
      - name: ${{ matrix.name }} fixture smoke
        shell: bash
        run: |
          set -euo pipefail
          python - <<'PY'
          from pathlib import Path
          import speakrs
          cache_dir = Path("_scratch/${{ matrix.cache_dir }}")
          cache_dir.mkdir(parents=True, exist_ok=True)
          prepared = speakrs.prepare(speakrs.ExecutionMode.${{ matrix.mode }}, cache_dir=cache_dir)
          pipeline = speakrs.Pipeline.from_prepared(prepared, speakrs.ExecutionMode.${{ matrix.mode }})
          result = pipeline.diarize_file("fixtures/test_short.wav", file_id="test_short")
          assert result.duration > 0
          assert result.mode == "${{ matrix.mode }}"
          PY
      - name: Upload ${{ matrix.name }} wheel
        uses: actions/upload-artifact@v6
        with:
          name: speakrs-${{ matrix.name }}-wheel
          path: wheelhouse/speakrs-${{ matrix.name }}/*.whl
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/package.yml around lines 342 - 393, The CUDA and MIGraphX
wheel jobs are duplicated and should be consolidated so they don’t drift apart.
Refactor the `python-cuda-wheel` and `python-migraphx-wheel` workflow sections
into a single matrix-based job or a reusable workflow, using the varying symbols
such as `ExecutionMode`, the runner labels, the cache directory names, and the
wheel output paths as matrix inputs. Keep the shared
build/install/smoke/artifact-upload steps in one place and parameterize only the
per-backend differences.

129-167: 🔒 Security & Privacy | 🔵 Trivial | 🏗️ Heavy lift

Add cache integrity safeguards for release artifact jobs.

The python-wheels job uses Swatinem/rust-cache@v2 (line 146) and produces wheel artifacts that are intended for public distribution. If the Rust cache is poisoned (e.g., via a compromised PR or cache collision), the resulting wheels could contain malicious code. Consider adding CIBW_BEFORE_BUILD or post-build hash verification, and evaluate whether cache should be disabled or scoped more tightly for release builds.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/package.yml around lines 129 - 167, The python-wheels
release job currently relies on Swatinem/rust-cache@v2, so tighten this path to
reduce cache-poisoning risk. In the python-wheels workflow job, update the Build
Python wheels step (pypa/cibuildwheel@v4.1.0) to add integrity checks such as
CIBW_BEFORE_BUILD or post-build wheel hash verification, and consider scoping or
disabling the Rust cache for this release artifact path. Keep the change
localized to the python-wheels job and its existing cache/build steps.

Source: Linters/SAST tools

crates/speakrs-sdk/src/models.rs (1)

452-460: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider using the hex crate instead of manual hex encoding.

The hex_lower function manually encodes bytes to hex. The hex crate is a well-known, audited dependency that provides hex::encode with identical behavior. This would reduce custom code and potential for subtle bugs.

♻️ Proposed refactor
-fn hex_lower(bytes: &[u8]) -> String {
-    const HEX: &[u8; 16] = b"0123456789abcdef";
-    let mut out = String::with_capacity(bytes.len() * 2);
-    for byte in bytes {
-        out.push(HEX[(byte >> 4) as usize] as char);
-        out.push(HEX[(byte & 0x0f) as usize] as char);
-    }
-    out
-}
+// In Cargo.toml: hex = "0.4"
+// Then replace call site:
+// hex::encode(hasher.finalize())
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/speakrs-sdk/src/models.rs` around lines 452 - 460, Replace the custom
byte-to-hex implementation in hex_lower with the standard hex crate encoder by
delegating to hex::encode, and remove the manual loop/lookup table. Update any
callers in models.rs to rely on the same hex_lower entry point so the behavior
stays identical while reducing custom code and maintenance risk.
crates/speakrs-sdk/src/facade.rs (1)

97-128: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

diarize_samples computes duration assuming 16kHz without validation.

Line 119 computes samples.len() as f64 / 16_000.0 as the duration, which is only correct for 16kHz mono input. While diarize_file ensures this via resampling, direct diarize_samples callers could pass samples at a different rate, producing incorrect duration values in the result. Consider documenting this requirement or accepting sample rate as a parameter.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/speakrs-sdk/src/facade.rs` around lines 97 - 128, The duration
calculation in diarize_samples assumes 16 kHz input by using samples.len() /
16_000.0, but this method can be called directly with arbitrary sample rates.
Update diarize_samples in facade.rs to either accept an explicit sample rate
parameter and use it for the duration passed into map_result, or clearly
enforce/document the 16 kHz requirement at the API boundary; reference the
diarize_samples method, map_result call, and the current duration computation so
the fix stays aligned with the existing pipeline flow.
crates/speakrs-sdk/src/audio.rs (1)

269-292: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Corrupted audio packets are silently skipped during decoding.

SymphoniaError::DecodeError(_) is matched with continue, which silently discards corrupted packets. While this is a common pattern for resilient audio playback, it can hide partial data corruption — the caller receives a decoded stream with gaps but no indication that packets were dropped. Consider emitting a warning log or tracking skipped packet count for observability.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/speakrs-sdk/src/audio.rs` around lines 269 - 292, The decode loop in
audio.rs currently swallows SymphoniaError::DecodeError(_) by continuing, which
hides dropped/corrupted packets from callers. Update the packet handling in the
decode match to record skipped decode errors or emit a warning through the
existing logging path so the number of discarded packets is observable, while
still allowing resilient decoding to continue. Use the decode match in the audio
decoding function to locate the change and keep the existing Ok(decoded) and
non-decode-error branches intact.
src/inference/embedding/load.rs (1)

27-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the use_native_coreml pattern into a shared helper.

The #[cfg(feature = "coreml")] / #[cfg(not(...))] guard for use_native_coreml is duplicated in sessions.rs (lines 72-75). Extracting it to a method on ExecutionMode or a shared function would provide a single source of truth and prevent divergence — if one site is updated without the other, ORT sessions could be incorrectly built or skipped.

♻️ Suggested helper extraction
 impl ExecutionMode {
+    pub(crate) fn use_native_coreml(self) -> bool {
+        #[cfg(feature = "coreml")]
+        { self.is_coreml() }
+        #[cfg(not(feature = "coreml"))]
+        { false }
+    }
+}
+```
+
+Then in `load.rs`:
+
+```diff
     mode.validate()?;
-    #[cfg(feature = "coreml")]
-    let use_native_coreml = mode.is_coreml();
-    #[cfg(not(feature = "coreml"))]
-    let use_native_coreml = false;
+    let use_native_coreml = mode.use_native_coreml();

+And in sessions.rs:
+
+```diff

  • #[cfg(feature = "coreml")]
  • let use_native_coreml = mode.is_coreml();
  • #[cfg(not(feature = "coreml"))]
  • let use_native_coreml = false;
  • let use_native_coreml = mode.use_native_coreml();
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @src/inference/embedding/load.rs around lines 27 - 34, The
use_native_coreml feature-guard logic is duplicated between load.rs and
sessions.rs, so extract it into a shared helper or an ExecutionMode method
and use that single source everywhere. Update load_embedding-related flow and
the sessions setup code to call the shared symbol instead of repeating the
#[cfg(feature = "coreml")] / #[cfg(not(feature = "coreml"))] pattern,
keeping mode.is_coreml() behavior centralized so ORT initialization decisions
stay consistent.


</details>

<!-- cr-comment:v1:7ccda2325b33c18bfd3b3bfb -->

</blockquote></details>
<details>
<summary>crates/speakrs-python/src/lib.rs (1)</summary><blockquote>

`722-728`: _🚀 Performance & Scalability_ | _🔵 Trivial_ | _🏗️ Heavy lift_

**Blocking `recv` holds the GIL for the entire wait duration.**

`recv` calls `SdkQueue::recv()` (a blocking channel receive) without releasing the GIL. This blocks all Python threads, including the event loop when `recv_async` delegates to `asyncio.to_thread`. Consider accepting a `py: Python<'_>` parameter and wrapping the blocking call in `py.allow_threads` so other Python threads can run during the wait.

<details>
<summary>♻️ Suggested refactor</summary>

```diff
     fn recv(&mut self, py: Python<'_>) -> PyResult<PyQueueResult> {
-        self.inner
-            .lock()
-            .map_err(|_| PyRuntimeError::new_err("queue lock was poisoned"))?
-            .recv()
-            .map(py_queue_result)
-            .map_err(sdk_py_err)
+        let guard = self
+            .inner
+            .lock()
+            .map_err(|_| PyRuntimeError::new_err("queue lock was poisoned"))?;
+        let result = py.allow_threads(|| guard.recv())?;
+        Ok(py_queue_result(result))
     }

This requires SdkQueue: Send so the MutexGuard can cross the allow_threads boundary. Verify that bound before implementing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/speakrs-python/src/lib.rs` around lines 722 - 728, The blocking recv
path in the PyO3 wrapper is holding the GIL while waiting on SdkQueue::recv(),
which blocks other Python threads. Update the recv method in PyQueue to accept a
Python<'_> token and move the blocking queue receive inside py.allow_threads so
the GIL is released during the wait; keep the existing error mapping around the
result. Before doing this, verify that SdkQueue: Send (and any types held
through the Mutex) can safely cross the allow_threads boundary.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bindings/android/speakrs/build.gradle.kts`:
- Around line 205-215: The sourceSets configuration in speakrs is using
Kotlin-specific directories without the Kotlin Android plugin applied, so the
build will fail during configuration. Update the module’s Gradle setup to apply
org.jetbrains.kotlin.android alongside com.android.library, or move the
sourceSets.main.kotlin configuration into a module that already applies the
Kotlin Android plugin; use sourceSets and the main/bundledOrt/lean blocks as the
location to verify the fix.

In `@bindings/python/README.md`:
- Around line 27-33: The wheel build docs are out of sync with CI: the README’s
`maturin build` examples in the CUDA and MIGraphX sections use a different
compatibility flag than the workflow. Update the README examples to match the CI
choice used for the corresponding wheel builds, referencing the `maturin build`
commands under the CUDA and MIGraphX instructions so local builds produce the
same wheel tags as CI.

In `@bindings/swift/Samples/DiarizeFile/DiarizeFile.xcodeproj/project.pbxproj`:
- Around line 67-87: The DiarizeFile target is missing its resource bundle
entry, so `test_short.wav` is not being packaged and `Bundle.main` can fail with
`missingAudioFixture`. Update the Xcode project for the `DiarizeFile` target to
include the resource build phase that contains `test_short.wav` (alongside the
existing `Sources` and `Frameworks` phases), and regenerate/commit the project
so the target’s resources are properly bundled.

In `@crates/speakrs-python/python/speakrs/cli.py`:
- Around line 10-27: The CLI in main currently lets SpeakrsError escape from the
prepare/Pipeline.from_prepared/diarize_file flow, which results in a raw
traceback instead of a clean failure. Update main to wrap those pipeline calls
in a try/except that catches speakrs.SpeakrsError, prints a concise error
message to stderr, and returns a non-zero exit code; also add the requested sys
and speakrs imports so the exception path and exit handling can be referenced
from main.

In `@crates/speakrs-sdk/src/audio.rs`:
- Around line 177-209: The resampling helper uses an unsupported Rubato
constructor, so update resample_mono_to_16khz to stop calling
InterleavedOwned::new_from and switch to the buffer constructor that exists in
rubato 3.0.0. Keep the rest of the flow in resample_mono_to_16khz unchanged,
including Async::new_poly, process_all_needed_output_len, and
process_all_into_buffer, and adjust the input buffer setup so it matches the
supported API.

In `@crates/speakrs-sdk/src/facade.rs`:
- Around line 248-257: The categorization in from_pipeline_error is relying on
brittle message substring matching, so update it to inspect the
speakrs::pipeline::PipelineError value directly instead of calling to_string()
and checking for "requires the `". Use the existing from_pipeline_error helper
to map concrete PipelineError variants or any exposed structured fields to
SdkErrorCategory::UnsupportedRuntime versus SdkErrorCategory::Pipeline,
preserving the stable error category contract.
- Around line 259-265: The `from_audio_error` helper currently maps every
`AudioDecodeError` variant to `SdkErrorCategory::AudioDecode`, so update it to
special-case `AudioDecodeError::Resample` and map that variant to
`SdkErrorCategory::AudioResample` while leaving the other decode errors
unchanged. Use the existing `from_audio_error` function and `SdkErrorCategory`
variants to keep the category consistent for downstream consumers.

In `@crates/speakrs-sdk/src/models.rs`:
- Around line 229-242: The cleanup_revision method is building a filesystem path
directly from the untrusted revision string, which allows path traversal. In
cleanup_revision, validate revision before joining it to
cache_dir/cache_repo_dir, rejecting any value containing path separators or
parent-directory segments like .., and only then call remove_dir_all on the
sanitized path. Use the existing cleanup_revision and
ModelPrepareError::CacheCleanup flow so invalid input fails safely before
deletion.

---

Nitpick comments:
In @.github/workflows/package.yml:
- Around line 104-128: Update the workflow’s `actions/checkout@v6` step in the
`rust-api-docs` job to set `persist-credentials: false`, since checkout
currently leaves the GitHub token in local git config. Apply the same setting to
any other `actions/checkout` steps in this workflow so release-artifact jobs do
not retain credentials unnecessarily.
- Around line 342-393: The CUDA and MIGraphX wheel jobs are duplicated and
should be consolidated so they don’t drift apart. Refactor the
`python-cuda-wheel` and `python-migraphx-wheel` workflow sections into a single
matrix-based job or a reusable workflow, using the varying symbols such as
`ExecutionMode`, the runner labels, the cache directory names, and the wheel
output paths as matrix inputs. Keep the shared
build/install/smoke/artifact-upload steps in one place and parameterize only the
per-backend differences.
- Around line 129-167: The python-wheels release job currently relies on
Swatinem/rust-cache@v2, so tighten this path to reduce cache-poisoning risk. In
the python-wheels workflow job, update the Build Python wheels step
(pypa/cibuildwheel@v4.1.0) to add integrity checks such as CIBW_BEFORE_BUILD or
post-build wheel hash verification, and consider scoping or disabling the Rust
cache for this release artifact path. Keep the change localized to the
python-wheels job and its existing cache/build steps.

In `@crates/speakrs-python/src/lib.rs`:
- Around line 722-728: The blocking recv path in the PyO3 wrapper is holding the
GIL while waiting on SdkQueue::recv(), which blocks other Python threads. Update
the recv method in PyQueue to accept a Python<'_> token and move the blocking
queue receive inside py.allow_threads so the GIL is released during the wait;
keep the existing error mapping around the result. Before doing this, verify
that SdkQueue: Send (and any types held through the Mutex) can safely cross the
allow_threads boundary.

In `@crates/speakrs-sdk/src/audio.rs`:
- Around line 269-292: The decode loop in audio.rs currently swallows
SymphoniaError::DecodeError(_) by continuing, which hides dropped/corrupted
packets from callers. Update the packet handling in the decode match to record
skipped decode errors or emit a warning through the existing logging path so the
number of discarded packets is observable, while still allowing resilient
decoding to continue. Use the decode match in the audio decoding function to
locate the change and keep the existing Ok(decoded) and non-decode-error
branches intact.

In `@crates/speakrs-sdk/src/facade.rs`:
- Around line 97-128: The duration calculation in diarize_samples assumes 16 kHz
input by using samples.len() / 16_000.0, but this method can be called directly
with arbitrary sample rates. Update diarize_samples in facade.rs to either
accept an explicit sample rate parameter and use it for the duration passed into
map_result, or clearly enforce/document the 16 kHz requirement at the API
boundary; reference the diarize_samples method, map_result call, and the current
duration computation so the fix stays aligned with the existing pipeline flow.

In `@crates/speakrs-sdk/src/models.rs`:
- Around line 452-460: Replace the custom byte-to-hex implementation in
hex_lower with the standard hex crate encoder by delegating to hex::encode, and
remove the manual loop/lookup table. Update any callers in models.rs to rely on
the same hex_lower entry point so the behavior stays identical while reducing
custom code and maintenance risk.

In `@src/inference/embedding/load.rs`:
- Around line 27-34: The `use_native_coreml` feature-guard logic is duplicated
between `load.rs` and `sessions.rs`, so extract it into a shared helper or an
`ExecutionMode` method and use that single source everywhere. Update
`load_embedding`-related flow and the `sessions` setup code to call the shared
symbol instead of repeating the `#[cfg(feature = "coreml")]` /
`#[cfg(not(feature = "coreml"))]` pattern, keeping `mode.is_coreml()` behavior
centralized so ORT initialization decisions stay consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d7772e8d-8a55-4e1c-a969-3c297ac3b988

📥 Commits

Reviewing files that changed from the base of the PR and between b9f9a62 and 0972f96.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • bindings/android/gradle/wrapper/gradle-wrapper.jar is excluded by !**/*.jar
📒 Files selected for processing (68)
  • .gitattributes
  • .github/workflows/package.yml
  • .gitignore
  • Cargo.toml
  • bindings/README.md
  • bindings/RELEASE.md
  • bindings/android/README.md
  • bindings/android/build.gradle.kts
  • bindings/android/gradle.properties
  • bindings/android/gradle/wrapper/gradle-wrapper.properties
  • bindings/android/gradlew
  • bindings/android/gradlew.bat
  • bindings/android/sample/build.gradle.kts
  • bindings/android/sample/src/main/AndroidManifest.xml
  • bindings/android/sample/src/main/java/com/avencera/speakrs/sample/MainActivity.kt
  • bindings/android/sample/src/main/res/values/styles.xml
  • bindings/android/settings.gradle.kts
  • bindings/android/speakrs/build.gradle.kts
  • bindings/android/speakrs/src/main/AndroidManifest.xml
  • bindings/python/README.md
  • bindings/python/speakrs-cuda/README.md
  • bindings/python/speakrs-cuda/pyproject.toml
  • bindings/python/speakrs-migraphx/README.md
  • bindings/python/speakrs-migraphx/pyproject.toml
  • bindings/swift/Package.swift
  • bindings/swift/README.md
  • bindings/swift/Samples/DiarizeFile/DiarizeFile.xcodeproj/project.pbxproj
  • bindings/swift/Samples/DiarizeFile/DiarizeFile.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  • bindings/swift/Samples/DiarizeFile/README.md
  • bindings/swift/Samples/DiarizeFile/Sources/DiarizeFileApp.swift
  • bindings/swift/Samples/DiarizeFile/project.yml
  • bindings/swift/Sources/Speakrs/speakrs_ffi.swift
  • bindings/swift/scripts/build-xcframework.sh
  • bindings/swift/scripts/package-xcframework.sh
  • crates/speakrs-ffi/Cargo.toml
  • crates/speakrs-ffi/src/bin/uniffi-bindgen.rs
  • crates/speakrs-ffi/src/lib.rs
  • crates/speakrs-ffi/uniffi.toml
  • crates/speakrs-python/API.md
  • crates/speakrs-python/Cargo.toml
  • crates/speakrs-python/README.md
  • crates/speakrs-python/examples/diarize_file.py
  • crates/speakrs-python/pyproject.toml
  • crates/speakrs-python/python/speakrs/__init__.py
  • crates/speakrs-python/python/speakrs/cli.py
  • crates/speakrs-python/src/lib.rs
  • crates/speakrs-python/tests/test_python_api.py
  • crates/speakrs-sdk/Cargo.toml
  • crates/speakrs-sdk/src/audio.rs
  • crates/speakrs-sdk/src/cancel.rs
  • crates/speakrs-sdk/src/dto.rs
  • crates/speakrs-sdk/src/error.rs
  • crates/speakrs-sdk/src/facade.rs
  • crates/speakrs-sdk/src/lib.rs
  • crates/speakrs-sdk/src/models.rs
  • src/clustering/plda.rs
  • src/inference.rs
  • src/inference/embedding.rs
  • src/inference/embedding/load.rs
  • src/inference/embedding/load/sessions.rs
  • src/inference/embedding/native/loaders.rs
  • src/inference/embedding/run.rs
  • src/inference/segmentation.rs
  • src/inference/segmentation/native.rs
  • src/inference/segmentation/run.rs
  • src/lib.rs
  • src/linalg.rs
  • src/pipeline/queued.rs

Comment on lines +205 to +215
sourceSets {
getByName("main") {
kotlin.directories.add(generatedKotlinDir.get().asFile.absolutePath)
}
getByName("bundledOrt") {
jniLibs.directories.add(generatedJniRoot.get().dir("bundledOrt").asFile.absolutePath)
}
getByName("lean") {
jniLibs.directories.add(generatedJniRoot.get().dir("lean").asFile.absolutePath)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n --glob 'bindings/android/**/build.gradle*' --glob 'bindings/android/**/settings.gradle*' \
  'org\.jetbrains\.kotlin\.android|kotlin\("android"\)|id\("org\.jetbrains\.kotlin\.android"\)'

Repository: avencera/speakrs

Length of output: 154


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- files ---\n'
git ls-files 'bindings/android/**' | sed -n '1,200p'

printf '\n--- kotlin android plugin references in repo ---\n'
rg -n 'org\.jetbrains\.kotlin\.android|kotlin\("android"\)|id\("org\.jetbrains\.kotlin\.android"\)|kotlin-android|android kotlin' .

printf '\n--- build files mentioning sourceSets/main/kotlin ---\n'
rg -n 'sourceSets|kotlin\.directories|generatedKotlinDir|generatedJniRoot' bindings/android

Repository: avencera/speakrs

Length of output: 245


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target build file and nearby build scripts.
wc -l bindings/android/speakrs/build.gradle.kts bindings/android/**/build.gradle.kts 2>/dev/null || true

printf '\n--- speakrs build.gradle.kts around sourceSets ---\n'
sed -n '1,260p' bindings/android/speakrs/build.gradle.kts

printf '\n--- other android gradle files ---\n'
find bindings/android -name 'build.gradle.kts' -o -name 'build.gradle' | sort | sed -n '1,200p'

Repository: avencera/speakrs

Length of output: 8629


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- root build files ---\n'
git ls-files '*build.gradle*' '*settings.gradle*' 'gradle/libs.versions.toml' | sed -n '1,200p'

printf '\n--- plugin declarations mentioning android/kotlin ---\n'
rg -n 'plugins \{|id\("com\.android\.library"\)|id\("org\.jetbrains\.kotlin\.android"\)|kotlin\("android"\)|alias\(libs\.plugins' . --glob '*build.gradle*' --glob 'gradle/libs.versions.toml' --glob '*settings.gradle*'

printf '\n--- speakrs build.gradle.kts full file stats and top section ---\n'
wc -l bindings/android/speakrs/build.gradle.kts
sed -n '1,120p' bindings/android/speakrs/build.gradle.kts

Repository: avencera/speakrs

Length of output: 245


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- target file excerpt ---\n'
sed -n '1,260p' bindings/android/speakrs/build.gradle.kts

printf '\n--- android-related build scripts and convention plugins ---\n'
find . \( -path './.git' -o -path './node_modules' -o -path './build' \) -prune -o \
  \( -name 'build.gradle.kts' -o -name 'build.gradle' -o -name 'settings.gradle.kts' -o -name 'settings.gradle' -o -name 'libs.versions.toml' -o -path './buildSrc/*' \) -print | sort | sed -n '1,250p'

printf '\n--- occurrences of com.android.library / kotlin android plugin / sourceSets.kotlin ---\n'
rg -n 'com\.android\.library|org\.jetbrains\.kotlin\.android|kotlin\("android"\)|sourceSets\.|generatedKotlinDir|generatedJniRoot' \
  . --glob '!**/.git/**' --glob '!**/node_modules/**' --glob '!**/build/**'

Repository: avencera/speakrs

Length of output: 245


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the exact lines around the reported block and surrounding plugin declarations.
nl -ba bindings/android/speakrs/build.gradle.kts | sed -n '1,260p'

printf '\n--- search for any implicit Kotlin Android application in the Android tree ---\n'
rg -n 'apply\(plugin = "org\.jetbrains\.kotlin\.android"\)|apply\(plugin = "kotlin-android"\)|id\("org\.jetbrains\.kotlin\.android"\)|kotlin\("android"\)' bindings/android .github gradle buildSrc \
  --glob '*.gradle*' --glob '*.kts' --glob '!*build/**' --glob '!*node_modules/**'

Repository: avencera/speakrs

Length of output: 194


🏁 Script executed:

#!/bin/bash
set -euo pipefail
set -o pipefail

printf '--- top-level gradle files ---\n'
find . -maxdepth 3 \( -name 'build.gradle.kts' -o -name 'build.gradle' -o -name 'settings.gradle.kts' -o -name 'settings.gradle' -o -name 'libs.versions.toml' -o -path './buildSrc/*' \) | sort

printf '\n--- plugin applications/references ---\n'
rg -n --hidden --glob '!**/.git/**' --glob '!**/node_modules/**' --glob '!**/build/**' \
  'org\.jetbrains\.kotlin\.android|kotlin\("android"\)|id\("org\.jetbrains\.kotlin\.android"\)|apply\(plugin\s*=\s*"org\.jetbrains\.kotlin\.android"\)|apply\(plugin\s*=\s*"kotlin-android"\)|com\.android\.library|com\.android\.application' \
  . 

printf '\n--- files under bindings/android ---\n'
find bindings/android -maxdepth 3 -type f | sort | sed -n '1,200p'

Repository: avencera/speakrs

Length of output: 245


🏁 Script executed:

#!/bin/bash
set -euo pipefail
set -o pipefail

printf '--- target file excerpt ---\n'
nl -ba bindings/android/speakrs/build.gradle.kts | sed -n '1,260p'

printf '\n--- gradle files and plugin declarations ---\n'
find . -maxdepth 4 \( -name 'build.gradle.kts' -o -name 'build.gradle' -o -name 'settings.gradle.kts' -o -name 'settings.gradle' -o -name 'libs.versions.toml' \) | sort | sed -n '1,200p'

printf '\n--- kotlin/android plugin references ---\n'
rg -n --hidden --glob '!**/.git/**' --glob '!**/node_modules/**' --glob '!**/build/**' \
  'org\.jetbrains\.kotlin\.android|kotlin\("android"\)|id\("org\.jetbrains\.kotlin\.android"\)|apply\(plugin\s*=\s*"org\.jetbrains\.kotlin\.android"\)|apply\(plugin\s*=\s*"kotlin-android"\)' \
  .

printf '\n--- sourceSets.kotlin / generated dirs references under bindings/android ---\n'
rg -n 'sourceSets|kotlin\.directories|generatedKotlinDir|generatedJniRoot' bindings/android

Repository: avencera/speakrs

Length of output: 245


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- bindings/android/build.gradle.kts ---'
cat -n bindings/android/build.gradle.kts | sed -n '1,220p'

echo
echo '--- plugin references under bindings/android ---'
grep -RInE 'org\.jetbrains\.kotlin\.android|kotlin\("android"\)|id\("org\.jetbrains\.kotlin\.android"\)|apply\(plugin *= *"org\.jetbrains\.kotlin\.android"\)|apply\(plugin *= *"kotlin-android"\)' bindings/android || true

echo
echo '--- sourceSets/kotlin references under bindings/android ---'
grep -RInE 'sourceSets|kotlin\.directories|generatedKotlinDir|generatedJniRoot' bindings/android || true

Repository: avencera/speakrs

Length of output: 1648


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- bindings/android/build.gradle.kts ---'
cat -n bindings/android/build.gradle.kts | sed -n '1,220p'

echo
echo '--- bindings/android/sample/build.gradle.kts ---'
cat -n bindings/android/sample/build.gradle.kts | sed -n '1,220p'

echo
echo '--- kotlin android plugin references anywhere in repository ---'
grep -RInE 'org\.jetbrains\.kotlin\.android|kotlin\("android"\)|id\("org\.jetbrains\.kotlin\.android"\)|apply\(plugin *= *"org\.jetbrains\.kotlin\.android"\)|apply\(plugin *= *"kotlin-android"\)' . --exclude-dir=.git --exclude-dir=build --exclude-dir=node_modules || true

Repository: avencera/speakrs

Length of output: 1952


Apply the Kotlin Android plugin here

bindings/android/speakrs/build.gradle.kts:205 uses sourceSets.main.kotlin, but this module only applies com.android.library. Add org.jetbrains.kotlin.android (or move this config into a module that already applies it), otherwise configuration will fail.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bindings/android/speakrs/build.gradle.kts` around lines 205 - 215, The
sourceSets configuration in speakrs is using Kotlin-specific directories without
the Kotlin Android plugin applied, so the build will fail during configuration.
Update the module’s Gradle setup to apply org.jetbrains.kotlin.android alongside
com.android.library, or move the sourceSets.main.kotlin configuration into a
module that already applies the Kotlin Android plugin; use sourceSets and the
main/bundledOrt/lean blocks as the location to verify the fix.

Comment thread bindings/python/README.md
Comment thread crates/speakrs-python/python/speakrs/cli.py Outdated
Comment on lines +177 to +209
pub fn resample_mono_to_16khz(
samples: &[f32],
sample_rate_hz: u32,
) -> Result<Vec<f32>, AudioDecodeError> {
let Some(plan) = ResamplePlan::to_sdk_rate(sample_rate_hz)? else {
return Ok(samples.to_vec());
};

if samples.is_empty() {
return Ok(Vec::new());
}

let input = InterleavedOwned::new_from(samples.to_vec(), 1, samples.len())
.map_err(|err| AudioDecodeError::Resample(err.to_string()))?;
let mut resampler = Async::<f32>::new_poly(
plan.ratio(),
1.1,
PolynomialDegree::Septic,
1024,
1,
FixedAsync::Input,
)
.map_err(|err| AudioDecodeError::Resample(err.to_string()))?;
let output_capacity = resampler.process_all_needed_output_len(samples.len());
let mut output = InterleavedOwned::new(0.0, 1, output_capacity);
let (_, output_len) = resampler
.process_all_into_buffer(&input, &mut output, samples.len(), None)
.map_err(|err| AudioDecodeError::Resample(err.to_string()))?;

let mut samples = output.take_data();
samples.truncate(output_len);
Ok(samples)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify rubato 3.0.0 API by checking the crate documentation
# Check if InterleavedOwned::new_from, process_all_needed_output_len, and process_all_into_buffer exist
rg -n "pub fn new_from" --type rust $(find / -path "*/rubato*/src/audioadapter*" 2>/dev/null | head -5) 2>/dev/null || echo "Could not find rubato source locally"
rg -n "process_all_needed_output_len\|process_all_into_buffer" --type rust $(find / -path "*/rubato*/src/*" 2>/dev/null | head -5) 2>/dev/null || echo "Could not find rubato source locally"

Repository: avencera/speakrs

Length of output: 191


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Dependency pin =="
rg -n 'rubato' Cargo.toml Cargo.lock crates -g '!target' || true

echo
echo "== Relevant code context =="
sed -n '1,260p' crates/speakrs-sdk/src/audio.rs

Repository: avencera/speakrs

Length of output: 8707


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Cargo.lock rubato entries =="
rg -n '^name = "rubato"$|^version = ' Cargo.lock -n -A2 -B2

echo
echo "== Locate rubato source in cargo registry =="
python3 - <<'PY'
import glob, os
cands = glob.glob(os.path.expanduser('~/.cargo/registry/src/*/rubato-*'))
print("\n".join(cands[:20]) if cands else "NO_RUBATO_SOURCE_FOUND")
PY

echo
echo "== Search rubato source for the relevant APIs =="
RUBATO_SRC="$(python3 - <<'PY'
import glob, os
cands = glob.glob(os.path.expanduser('~/.cargo/registry/src/*/rubato-*'))
print(cands[0] if cands else "")
PY
)"
if [ -n "$RUBATO_SRC" ] && [ -d "$RUBATO_SRC" ]; then
  rg -n "struct InterleavedOwned|impl InterleavedOwned|fn new_from|fn new\(|fn new_poly|fn process_all_needed_output_len|fn process_all_into_buffer|trait Resampler" "$RUBATO_SRC/src" || true
else
  echo "rubato source not found in cargo registry cache"
fi

Repository: avencera/speakrs

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import glob, os, pathlib, re

cands = glob.glob(os.path.expanduser('~/.cargo/registry/src/*/rubato-3.0.0'))
if not cands:
    print("NO_RUBATO_3_0_0_SOURCE_FOUND")
    raise SystemExit(0)

root = cands[0]
print(root)

targets = [
    "src/lib.rs",
    "src/async_resampler.rs",
    "src/audioadapter_buffers/owned.rs",
    "src/audioadapter_buffers/mod.rs",
]

for rel in targets:
    p = pathlib.Path(root) / rel
    if p.exists():
        print(f"\n== {rel} ==")
        text = p.read_text(errors="ignore").splitlines()
        for needle in [
            "pub struct InterleavedOwned",
            "impl InterleavedOwned",
            "fn new_from",
            "fn new(",
            "fn new_poly",
            "fn process_all_needed_output_len",
            "fn process_all_into_buffer",
            "trait Resampler",
        ]:
            for i, line in enumerate(text, 1):
                if needle in line:
                    start = max(1, i - 3)
                    end = min(len(text), i + 8)
                    print(f"-- match {needle!r} at line {i} --")
                    for j in range(start, end + 1):
                        print(f"{j}:{text[j-1]}")
                    break
    else:
        print(f"\n== MISSING {rel} ==")
PY

Repository: avencera/speakrs

Length of output: 183


🌐 Web query:

rubato 3.0.0 docs.rs InterleavedOwned new_from Async new_poly process_all_needed_output_len process_all_into_buffer

💡 Result:

In rubato 3.0.0, the components you mentioned are part of the core API for audio resampling. Below is a summary of their roles and usage based on the official documentation [1][2]. InterleavedOwned InterleavedOwned is a struct that wraps a Vec to store interleaved audio samples [1]. It is used as the return type for the convenience method Resampler::process, which allocates a new output buffer for each call [1][3]. Because it involves heap allocation, it is generally not recommended for real-time applications [1][2]. Async Async is one of the available resampler types (implementing the Resampler trait) that uses sinc interpolation [4]. It is designed for scenarios where sample rate conversion is required [4]. process_all_needed_output_len This method calculates the minimal length of the output buffer required to process an input buffer of a given length (input_len) using the process_all_into_buffer method [2][3]. It accounts for the expected output size (based on the resample ratio) plus additional buffer space required by the resampler, including its internal delay [3]. process_all_into_buffer This method is a convenience function that processes entire audio clips of arbitrary length [1][2]. It repeatedly calls process_into_buffer internally until all input frames have been processed [2]. It writes the result into a provided, pre-allocated output buffer (which must implement the AdapterMut trait) and automatically trims the initial silence caused by the resampler's delay [2]. Technical Context In rubato 3.0.0, the Resampler trait provides these methods to handle different use cases [1]: 1. Real-time processing: Use process_into_buffer with a pre-allocated buffer (e.g., using an Adapter) to avoid unpredictable heap allocations [2]. 2. Non-real-time/Batch processing: Use process_all_into_buffer for convenience when the entire audio clip is available in memory [1][2]. 3. Simple/Convenience usage: Use process if the overhead of allocation per call is acceptable [1][2]. For real-time usage, you should pre-calculate the required buffer size using process_all_needed_output_len and pass a pre-allocated output buffer to process_all_into_buffer or process_into_buffer to ensure predictable performance [2][3].

Citations:


🌐 Web query:

rubato 3.0.0 Async::new_poly signature InterleavedOwned::new_from process_all_needed_output_len process_all_into_buffer docs.rs

💡 Result:

In rubato 3.0.0, the Async::new_poly constructor and the Resampler trait methods for buffer processing have specific signatures and usage patterns as follows: Async::new_poly signature The Async::new_poly constructor is used to create an asynchronous polynomial resampler [1]. Its signature is as follows: pub fn new_poly( resample_ratio: f64, max_relative_ratio: f64, degree: PolynomialDegree, chunk_size: usize, channels: usize, fixed: FixedAsync) -> ResampleResult This constructor accepts the resampling ratio, maximum relative ratio, polynomial interpolation degree (e.g., PolynomialDegree::Septic), chunk size, number of channels, and a FixedAsync variant (FixedAsync::Input or FixedAsync::Output) to determine if input or output size is fixed [1][2]. Resampler trait methods The methods process_all_needed_output_len and process_all_into_buffer are provided by the Resampler trait [3][4]. 1. process_all_needed_output_len(input_len: usize) -> usize: This method calculates the minimal length of the output buffer required to process an input clip of a specific length using process_all_into_buffer [3][4]. 2. process_all_into_buffer<'a, 'b>(&mut self, buffer_in: &dyn Adapter<'a, T>, buffer_out: &mut dyn AdapterMut<'b, T>, input_len: usize, active_channels_mask: Option<&[bool]>) -> ResampleResult<(usize, usize)>: This is a convenience method that repeatedly calls process_into_buffer to process an entire input buffer [3][4]. It returns the number of input frames processed and the number of output frames produced [3]. Regarding InterleavedOwned::new_from, this method is part of the audioadapter ecosystem (often used with or re-exported by rubato) for creating buffers, but the primary API for processing in rubato typically uses the Adapter/AdapterMut traits for memory-efficient handling rather than direct interaction with InterleavedOwned unless specifically initializing your own buffers [1][3][5]. top_results: [1][3][4][6]

Citations:


🌐 Web query:

rubato 3.0.0 audioadapter-buffers InterleavedOwned::new_from docs

💡 Result:

In rubato 3.0.0, InterleavedOwned::new_from is not a standard part of the public API for the main resampler structs. Instead, the library uses InterleavedOwned::new to allocate buffers, as seen in the library's internal source code [1]. The InterleavedOwned struct is provided by the audioadapter-buffers crate (which rubato depends on) [2]. Based on the rubato 3.0.0 source code, you typically initialize an InterleavedOwned buffer using the new constructor [1]: InterleavedOwned::::new(fill_value, channels, frames) In this call: - fill_value: The initial value to populate the buffer (e.g., T::coerce_from(0.0)) [1]. - channels: The number of audio channels [1]. - frames: The number of frames to allocate [1]. If you are looking for functions to wrap existing data, the rubato documentation and examples typically recommend using InterleavedSlice::new for immutable slices or InterleavedSlice::new_mut for mutable slices [3][4]. These adapters allow you to work with existing memory without forcing an allocation, which is generally preferred for real-time audio processing [5][6]. The 3.0.0 release of rubato primarily introduced changes to lifetimes in process_into_buffer and performance improvements, but the fundamental buffer handling patterns using the audioadapter ecosystem remain consistent with these established methods [3][7].

Citations:


🌐 Web query:

site:docs.rs/rubato 3.0.0 "InterleavedOwned::new_from"

💡 Result:

There is no public API or documented method named InterleavedOwned::new_from in the rubato or audioadapter-buffers crates [1][2]. In the rubato crate, the InterleavedOwned struct is used as an output buffer for resamplers [1][2]. The standard method for creating an instance of this struct is InterleavedOwned::new, which takes a default value, the number of channels, and the number of frames as arguments [1]. Example usage found in the source code: let mut buffer_out = InterleavedOwned::::new(T::coerce_from(0.0), channels, frames); The rubato crate uses the audioadapter crate to handle input and output buffers [2][3]. For realtime applications, it is recommended to avoid the convenience methods that allocate memory (such as those returning InterleavedOwned) and instead use pre-allocated buffers with process_into_buffer [2][4].

Citations:


Replace InterleavedOwned::new_from with a supported buffer constructor rubato 3.0.0 does not expose InterleavedOwned::new_from; the Async::new_poly, process_all_needed_output_len, and process_all_into_buffer calls match the 3.0.0 API.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/speakrs-sdk/src/audio.rs` around lines 177 - 209, The resampling
helper uses an unsupported Rubato constructor, so update resample_mono_to_16khz
to stop calling InterleavedOwned::new_from and switch to the buffer constructor
that exists in rubato 3.0.0. Keep the rest of the flow in resample_mono_to_16khz
unchanged, including Async::new_poly, process_all_needed_output_len, and
process_all_into_buffer, and adjust the input buffer setup so it matches the
supported API.

Comment thread crates/speakrs-sdk/src/facade.rs
Comment thread crates/speakrs-sdk/src/facade.rs
Comment thread crates/speakrs-sdk/src/models.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
.github/workflows/package.yml (1)

143-143: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rust toolchain version inconsistency between Linux and macOS builds.

Linux builds pin Rust 1.88.0 via CIBW_BEFORE_ALL_LINUX (line 162), while macOS builds use unpinned stable from dtolnay/rust-toolchain@stable (line 143). Since cibuildwheel runs macOS builds natively on the runner (no container), macOS wheels compile with whatever "stable" resolves to at CI time, while Linux wheels always use 1.88.0. This divergence could produce different codegen or behavior across platforms if a new Rust release changes defaults.

Consider pinning the macOS toolchain to the same version, e.g. dtolnay/rust-toolchain@1.88.0, or adding CIBW_BEFORE_ALL_MACOS for consistency.

Also applies to: 162-162

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/package.yml at line 143, The Rust toolchain is
inconsistent between the macOS and Linux build paths in the package workflow, so
update the macOS setup to match the pinned Linux version used by
CIBW_BEFORE_ALL_LINUX. Change the dtolnay/rust-toolchain action in the workflow
to a fixed 1.88.0 reference, or otherwise make CIBW_BEFORE_ALL_MACOS install the
same Rust version, so the cibuildwheel jobs build with a consistent toolchain
across platforms.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In @.github/workflows/package.yml:
- Line 143: The Rust toolchain is inconsistent between the macOS and Linux build
paths in the package workflow, so update the macOS setup to match the pinned
Linux version used by CIBW_BEFORE_ALL_LINUX. Change the dtolnay/rust-toolchain
action in the workflow to a fixed 1.88.0 reference, or otherwise make
CIBW_BEFORE_ALL_MACOS install the same Rust version, so the cibuildwheel jobs
build with a consistent toolchain across platforms.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d2a5a393-7293-4244-97f1-ff90c1390ec0

📥 Commits

Reviewing files that changed from the base of the PR and between 0972f96 and 30474d7.

📒 Files selected for processing (16)
  • .github/workflows/package.yml
  • Cargo.toml
  • bindings/RELEASE.md
  • bindings/python/speakrs-cuda/pyproject.toml
  • bindings/python/speakrs-migraphx/pyproject.toml
  • crates/speakrs-ffi/src/lib.rs
  • crates/speakrs-python/Cargo.toml
  • crates/speakrs-python/pyproject.toml
  • crates/speakrs-python/python/speakrs/__init__.py
  • crates/speakrs-python/src/lib.rs
  • crates/speakrs-python/tests/test_python_api.py
  • crates/speakrs-sdk/Cargo.toml
  • crates/speakrs-sdk/src/dto.rs
  • crates/speakrs-sdk/src/facade.rs
  • crates/speakrs-sdk/src/lib.rs
  • src/pipeline/queued.rs
🚧 Files skipped from review as they are similar to previous changes (12)
  • crates/speakrs-python/Cargo.toml
  • bindings/python/speakrs-cuda/pyproject.toml
  • crates/speakrs-sdk/src/lib.rs
  • crates/speakrs-sdk/Cargo.toml
  • Cargo.toml
  • bindings/python/speakrs-migraphx/pyproject.toml
  • crates/speakrs-python/tests/test_python_api.py
  • crates/speakrs-sdk/src/facade.rs
  • crates/speakrs-ffi/src/lib.rs
  • crates/speakrs-python/python/speakrs/init.py
  • crates/speakrs-sdk/src/dto.rs
  • crates/speakrs-python/src/lib.rs

Ensure Android debug variants build the Rust artifacts and generated UniFFI Kotlin before packaging by adding dependencies for `preBundledOrtDebugBuild` and `preLeanDebugBuild`. Also make all Kotlin compile tasks depend on `generateUniFfiKotlin` so generated bindings are always available during compilation.
Return PipelineConfig as public Python dataclass values so tests
can inspect mode defaults, and run queue ingestion with detached
GIL sections to avoid blocking Python threads.

Also extend CI to exercise pure-linalg and validate downloadable
model fixtures with a tiny end-to-end diarization check during
wheel testing.,
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant