Skip to content

feat(M6a): Pipeline + Profile API (additive) for v1.0 - #15

Merged
ekhodzitsky merged 10 commits into
masterfrom
feat/m6a-pipeline-v2
May 8, 2026
Merged

ekhodzitsky merged 10 commits into
masterfrom
feat/m6a-pipeline-v2

Conversation

@ekhodzitsky

Copy link
Copy Markdown
Owner

Summary

Seventh v1.0 milestone (split: M6a = additive new Pipeline; M6b = CLI/FFI/Python migration + legacy delete). Builds on M0–M5. Introduces polyvoice::pipeline_v2 module with Pipeline::builder() API wiring the full M1–M5 stack into a single end-to-end run(&samples, SampleRate) -> Result<DiarizationResult, PipelineError> call.

Zero behavioral change for existing users. Legacy polyvoice::Pipeline is untouched. New API exposed under polyvoice::PipelineV2 (M6b will rename + delete legacy).

Highlights

New module polyvoice::pipeline_v2

  • Pipeline::builder() returning PipelineBuilder with fluent setters: .profile(Profile), .with_models_from(ModelRegistry), .with_segmenter/embedder/clusterer/resegmenter(), plus convenience setters for resegment_overlap, embedder_pool_size, max_speakers. Validated .build() resolves Mobile/Balanced ONNX through ModelRegistry (M0) or accepts caller-supplied trait objects for Custom.
  • PipelineConfig (spec §5.2) + ClustererKind (NmeSc default, Ahc with threshold) + ExecutionProvider (Cpu/CoreMl/Nnapi/Cuda/XnnPack with platform-aware auto()).
  • Pipeline::run() orchestrates: sample-rate guard → M1 powerset segmenter → extract_overlap_time_ranges → per-primary-chunk apply_overlap_mask (M2 helper) + embedder.embed + l2_normalize → M3 NME-SC clustering → compute_centroids → optional M4 OverlapResegmenter → defensive sort → min_speech_secs filter → legacy merge_segments → DiarizationResult.
  • Typed PipelineError (8 variants); typed ConfigError for builder validation.

New Cargo feature pipeline_v2 (default-on, requires onnx + segmentation + embedder + clusterer + resegmentation). compile_error! defense-in-depth guards against half-wired feature combos.

Public re-exports in lib.rs: polyvoice::PipelineV2 (= pipeline_v2::Pipeline), PipelineBuilder, PipelineConfig, ClustererKind, ExecutionProvider, ConfigError, PipelineV2Error. Legacy polyvoice::Pipeline unchanged.

Test plan

  • 168 lib tests pass (M0:69 + M1:34 + M2:15 + M3:10 + M4:25 + M5:0 + M6a:15)
  • 7 synthetic integration tests in tests/pipeline_v2_synthetic_test.rs (builder validation, end-to-end Custom profile run, sorted output, overlap toggle, silence handling, unsupported sample rate)
  • #[ignore] E2E test in tests/pipeline_v2_e2e_test.rs (Balanced profile, real ONNX via ModelRegistry, single VoxConverse-test WAV)
  • 36 doc tests pass
  • cargo clippy --all-features --tests -- -D warnings clean
  • cargo fmt --check clean
  • cargo check --target wasm32-unknown-unknown --no-default-features --features resegmentation --lib clean (pipeline_v2 gated out automatically)
  • scripts/release-gate.sh exit 2 (PASS 5, FAIL 0, PENDING 9 — all pending items are M6b/M8/M9)
  • CI matrix on PR

Code review findings (Task 5 fixups merged into branch)

  • CRITICAL: zero-length primary segments were skipped during embedding but left in primary_segments, causing later label-by-index zip to assign wrong speakers to every segment after the skip. Fixed by tracking parallel valid_segments Vec.
  • HIGH: spec §5 explicitly required apply_overlap_mask on every primary chunk so overlap voices don't contaminate primary speaker embeddings. Fixed.
  • MEDIUM: build_overlap_inputs fallback now picks nearest primary turn by midpoint distance.
  • MEDIUM: defensive total_cmp sort before merge_segments so any Resegmenter impl that returns unsorted output is handled.

Out of scope (M6b)

  • Removing src/pipeline.rs, src/offline.rs, OfflineDiarizer, DiarizationConfig, VadConfig, EnergyVad, VoiceActivityDetector, segment_speech, DummyExtractor, OnnxEmbeddingExtractor, compute_fbank privatization.
  • Renaming pipeline_v2 → pipeline, PipelineV2 → Pipeline.
  • CLI polyvoice diarize --profile mobile|balanced rewrite.
  • FFI rewrite (src/ffi.rs).
  • Python pyo3 bindings rewrite (python/src/lib.rs).
  • polyvoice-bench rewrite.
  • E2E DER on VoxConverse-test/AMI through polyvoice-bench → tests/der_baseline.json.
  • Migration guide docs/MIGRATING-FROM-0.5.md.
  • OnlineDiarizer deprecation annotation.

References

🤖 Generated with Claude Code

ekhodzitsky and others added 10 commits May 8, 2026 07:13
…ider

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ask 4)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… validation

Adds src/pipeline_v2/mocks.rs with MockSegmenter, MockEmbedder,
MockClusterer, PassThroughResegmenter, and raw_segment() helper used by
builder and future integration tests.

Also fixes validate() check order in PipelineBuilder: custom-component
checks now run before missing-registry check so the correct
ConfigError variant is returned when a non-Custom profile receives a
custom component (validates_balanced_with_custom_segmenter_errors).

All 8 builder + 3 config tests pass; clippy clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…uild()

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ask + sort guard

Four findings from the M6a Task 5 code-quality review:

1. CRITICAL: zero-length primary segments were skipped during embedding but
   left in primary_segments, causing later label-by-index zip to assign
   wrong speakers to every segment after the skip. Track a parallel
   valid_segments Vec and zip with labels off that.

2. HIGH: spec §5 requires apply_overlap_mask on every primary chunk so the
   primary speaker's embedding isn't contaminated by intersecting overlap
   regions. Compute chunk-relative overlap windows and call
   embedder::apply_overlap_mask before embedder.embed.

3. MEDIUM: build_overlap_inputs fallback now picks the nearest primary
   turn by midpoint distance instead of start-time distance — handles
   overlap regions at the tail of long primary turns correctly.

4. MEDIUM: defensive sort before merge_segments so any Resegmenter
   implementation that returns unsorted output is handled safely.

15 lib tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…) flow

7 tests covering builder validation (Mobile missing registry, Custom missing
components), end-to-end Custom profile flow (two speakers, sorted output,
silence handling, unsupported sample rate), and overlap-resegmentation
toggle. Pipeline doesn't derive Debug (Box<dyn Trait> components prevent it),
so error checks use explicit match instead of .unwrap_err().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ngelog

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…plan

Captures the M6a design (additive pipeline_v2 module parallel to legacy)
and the 7-task TDD implementation plan that produced the m6a-complete tag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two CI failures from PR #15:

1. Python wheel builds (mac/ubuntu/windows × 3.11/3.12/3.13) failed because
   the python crate doesn't enable `download`. pipeline_v2/builder.rs imports
   `crate::models::{ModelRegistry, RegistryError}` which is gated behind
   `feature = "download"`. Made `pipeline_v2 = ["download"]` so the feature
   pulls its own dependency.

2. fmt failed on src/pipeline_v2/mocks.rs (rustfmt rewrites the file when
   first formatting). Applied `cargo fmt` — no semantic changes.

15 lib tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ekhodzitsky
ekhodzitsky merged commit 9de7802 into master May 8, 2026
42 of 43 checks passed
@ekhodzitsky
ekhodzitsky deleted the feat/m6a-pipeline-v2 branch May 8, 2026 07:59
ekhodzitsky added a commit that referenced this pull request Jul 31, 2026
## Summary

Seventh v1.0 milestone (split: M6a = additive new Pipeline; M6b = CLI/FFI/Python migration + legacy delete). Builds on M0–M5. Introduces `polyvoice::pipeline_v2` module with `Pipeline::builder()` API wiring the full M1–M5 stack into a single end-to-end `run(&samples, SampleRate) -> Result<DiarizationResult, PipelineError>` call.

**Zero behavioral change for existing users.** Legacy `polyvoice::Pipeline` is untouched. New API exposed under `polyvoice::PipelineV2` (M6b will rename + delete legacy).

## Highlights

**New module `polyvoice::pipeline_v2`**
- `Pipeline::builder()` returning `PipelineBuilder` with fluent setters: `.profile(Profile)`, `.with_models_from(ModelRegistry)`, `.with_segmenter/embedder/clusterer/resegmenter()`, plus convenience setters for `resegment_overlap`, `embedder_pool_size`, `max_speakers`. Validated `.build()` resolves Mobile/Balanced ONNX through `ModelRegistry` (M0) or accepts caller-supplied trait objects for Custom.
- `PipelineConfig` (spec §5.2) + `ClustererKind` (NmeSc default, Ahc with threshold) + `ExecutionProvider` (Cpu/CoreMl/Nnapi/Cuda/XnnPack with platform-aware `auto()`).
- `Pipeline::run()` orchestrates: sample-rate guard → M1 powerset segmenter → `extract_overlap_time_ranges` → per-primary-chunk `apply_overlap_mask` (M2 helper) + `embedder.embed` + `l2_normalize` → M3 NME-SC clustering → `compute_centroids` → optional M4 `OverlapResegmenter` → defensive sort → `min_speech_secs` filter → legacy `merge_segments` → `DiarizationResult`.
- Typed `PipelineError` (8 variants); typed `ConfigError` for builder validation.

**New Cargo feature `pipeline_v2`** (default-on, requires `onnx + segmentation + embedder + clusterer + resegmentation`). `compile_error!` defense-in-depth guards against half-wired feature combos.

**Public re-exports** in `lib.rs`: `polyvoice::PipelineV2` (= `pipeline_v2::Pipeline`), `PipelineBuilder`, `PipelineConfig`, `ClustererKind`, `ExecutionProvider`, `ConfigError`, `PipelineV2Error`. Legacy `polyvoice::Pipeline` unchanged.

## Test plan

- [x] 168 lib tests pass (M0:69 + M1:34 + M2:15 + M3:10 + M4:25 + M5:0 + M6a:15)
- [x] 7 synthetic integration tests in `tests/pipeline_v2_synthetic_test.rs` (builder validation, end-to-end Custom profile run, sorted output, overlap toggle, silence handling, unsupported sample rate)
- [x] `#[ignore]` E2E test in `tests/pipeline_v2_e2e_test.rs` (Balanced profile, real ONNX via `ModelRegistry`, single VoxConverse-test WAV)
- [x] 36 doc tests pass
- [x] `cargo clippy --all-features --tests -- -D warnings` clean
- [x] `cargo fmt --check` clean
- [x] `cargo check --target wasm32-unknown-unknown --no-default-features --features resegmentation --lib` clean (pipeline_v2 gated out automatically)
- [x] `scripts/release-gate.sh` exit 2 (PASS 5, FAIL 0, PENDING 9 — all pending items are M6b/M8/M9)
- [ ] CI matrix on PR

## Code review findings (Task 5 fixups merged into branch)

- CRITICAL: zero-length primary segments were skipped during embedding but left in `primary_segments`, causing later label-by-index zip to assign wrong speakers to every segment after the skip. Fixed by tracking parallel `valid_segments` Vec.
- HIGH: spec §5 explicitly required `apply_overlap_mask` on every primary chunk so overlap voices don't contaminate primary speaker embeddings. Fixed.
- MEDIUM: `build_overlap_inputs` fallback now picks nearest primary turn by midpoint distance.
- MEDIUM: defensive `total_cmp` sort before `merge_segments` so any `Resegmenter` impl that returns unsorted output is handled.

## Out of scope (M6b)

- Removing `src/pipeline.rs`, `src/offline.rs`, `OfflineDiarizer`, `DiarizationConfig`, `VadConfig`, `EnergyVad`, `VoiceActivityDetector`, `segment_speech`, `DummyExtractor`, `OnnxEmbeddingExtractor`, `compute_fbank` privatization.
- Renaming `pipeline_v2 → pipeline`, `PipelineV2 → Pipeline`.
- CLI `polyvoice diarize --profile mobile|balanced` rewrite.
- FFI rewrite (`src/ffi.rs`).
- Python pyo3 bindings rewrite (`python/src/lib.rs`).
- `polyvoice-bench` rewrite.
- E2E DER on VoxConverse-test/AMI through polyvoice-bench → `tests/der_baseline.json`.
- Migration guide `docs/MIGRATING-FROM-0.5.md`.
- `OnlineDiarizer` deprecation annotation.

## References

- Spec: `docs/superpowers/specs/2026-05-07-m6a-pipeline-v2-design.md`
- Plan: `docs/superpowers/plans/2026-05-07-m6a-pipeline-v2-plan.md`
- Roadmap: `docs/superpowers/specs/2026-05-07-perfect-diarization-roadmap-v1-design.md` §3.1, §5.2, §5.4, §10.1
- Tag: `m6a-complete`
- Builds on: PR #9, #10, #11, #12, #13, #14

🤖 Generated with [Claude Code](https://claude.com/claude-code)
ekhodzitsky added a commit that referenced this pull request Jul 31, 2026
## Summary

Seventh v1.0 milestone (split: M6a = additive new Pipeline; M6b = CLI/FFI/Python migration + legacy delete). Builds on M0–M5. Introduces `polyvoice::pipeline_v2` module with `Pipeline::builder()` API wiring the full M1–M5 stack into a single end-to-end `run(&samples, SampleRate) -> Result<DiarizationResult, PipelineError>` call.

**Zero behavioral change for existing users.** Legacy `polyvoice::Pipeline` is untouched. New API exposed under `polyvoice::PipelineV2` (M6b will rename + delete legacy).

## Highlights

**New module `polyvoice::pipeline_v2`**
- `Pipeline::builder()` returning `PipelineBuilder` with fluent setters: `.profile(Profile)`, `.with_models_from(ModelRegistry)`, `.with_segmenter/embedder/clusterer/resegmenter()`, plus convenience setters for `resegment_overlap`, `embedder_pool_size`, `max_speakers`. Validated `.build()` resolves Mobile/Balanced ONNX through `ModelRegistry` (M0) or accepts caller-supplied trait objects for Custom.
- `PipelineConfig` (spec §5.2) + `ClustererKind` (NmeSc default, Ahc with threshold) + `ExecutionProvider` (Cpu/CoreMl/Nnapi/Cuda/XnnPack with platform-aware `auto()`).
- `Pipeline::run()` orchestrates: sample-rate guard → M1 powerset segmenter → `extract_overlap_time_ranges` → per-primary-chunk `apply_overlap_mask` (M2 helper) + `embedder.embed` + `l2_normalize` → M3 NME-SC clustering → `compute_centroids` → optional M4 `OverlapResegmenter` → defensive sort → `min_speech_secs` filter → legacy `merge_segments` → `DiarizationResult`.
- Typed `PipelineError` (8 variants); typed `ConfigError` for builder validation.

**New Cargo feature `pipeline_v2`** (default-on, requires `onnx + segmentation + embedder + clusterer + resegmentation`). `compile_error!` defense-in-depth guards against half-wired feature combos.

**Public re-exports** in `lib.rs`: `polyvoice::PipelineV2` (= `pipeline_v2::Pipeline`), `PipelineBuilder`, `PipelineConfig`, `ClustererKind`, `ExecutionProvider`, `ConfigError`, `PipelineV2Error`. Legacy `polyvoice::Pipeline` unchanged.

## Test plan

- [x] 168 lib tests pass (M0:69 + M1:34 + M2:15 + M3:10 + M4:25 + M5:0 + M6a:15)
- [x] 7 synthetic integration tests in `tests/pipeline_v2_synthetic_test.rs` (builder validation, end-to-end Custom profile run, sorted output, overlap toggle, silence handling, unsupported sample rate)
- [x] `#[ignore]` E2E test in `tests/pipeline_v2_e2e_test.rs` (Balanced profile, real ONNX via `ModelRegistry`, single VoxConverse-test WAV)
- [x] 36 doc tests pass
- [x] `cargo clippy --all-features --tests -- -D warnings` clean
- [x] `cargo fmt --check` clean
- [x] `cargo check --target wasm32-unknown-unknown --no-default-features --features resegmentation --lib` clean (pipeline_v2 gated out automatically)
- [x] `scripts/release-gate.sh` exit 2 (PASS 5, FAIL 0, PENDING 9 — all pending items are M6b/M8/M9)
- [ ] CI matrix on PR

## Code review findings (Task 5 fixups merged into branch)

- CRITICAL: zero-length primary segments were skipped during embedding but left in `primary_segments`, causing later label-by-index zip to assign wrong speakers to every segment after the skip. Fixed by tracking parallel `valid_segments` Vec.
- HIGH: spec §5 explicitly required `apply_overlap_mask` on every primary chunk so overlap voices don't contaminate primary speaker embeddings. Fixed.
- MEDIUM: `build_overlap_inputs` fallback now picks nearest primary turn by midpoint distance.
- MEDIUM: defensive `total_cmp` sort before `merge_segments` so any `Resegmenter` impl that returns unsorted output is handled.

## Out of scope (M6b)

- Removing `src/pipeline.rs`, `src/offline.rs`, `OfflineDiarizer`, `DiarizationConfig`, `VadConfig`, `EnergyVad`, `VoiceActivityDetector`, `segment_speech`, `DummyExtractor`, `OnnxEmbeddingExtractor`, `compute_fbank` privatization.
- Renaming `pipeline_v2 → pipeline`, `PipelineV2 → Pipeline`.
- CLI `polyvoice diarize --profile mobile|balanced` rewrite.
- FFI rewrite (`src/ffi.rs`).
- Python pyo3 bindings rewrite (`python/src/lib.rs`).
- `polyvoice-bench` rewrite.
- E2E DER on VoxConverse-test/AMI through polyvoice-bench → `tests/der_baseline.json`.
- Migration guide `docs/MIGRATING-FROM-0.5.md`.
- `OnlineDiarizer` deprecation annotation.

## References

- Spec: `docs/superpowers/specs/2026-05-07-m6a-pipeline-v2-design.md`
- Plan: `docs/superpowers/plans/2026-05-07-m6a-pipeline-v2-plan.md`
- Roadmap: `docs/superpowers/specs/2026-05-07-perfect-diarization-roadmap-v1-design.md` §3.1, §5.2, §5.4, §10.1
- Tag: `m6a-complete`
- Builds on: PR #9, #10, #11, #12, #13, #14
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