Repository navigation
feat(M6a): Pipeline + Profile API (additive) for v1.0 - #15
Merged
Merged
Conversation
…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>
7 of 8 tasks
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Seventh v1.0 milestone (split: M6a = additive new Pipeline; M6b = CLI/FFI/Python migration + legacy delete). Builds on M0–M5. Introduces
polyvoice::pipeline_v2module withPipeline::builder()API wiring the full M1–M5 stack into a single end-to-endrun(&samples, SampleRate) -> Result<DiarizationResult, PipelineError>call.Zero behavioral change for existing users. Legacy
polyvoice::Pipelineis untouched. New API exposed underpolyvoice::PipelineV2(M6b will rename + delete legacy).Highlights
New module
polyvoice::pipeline_v2Pipeline::builder()returningPipelineBuilderwith fluent setters:.profile(Profile),.with_models_from(ModelRegistry),.with_segmenter/embedder/clusterer/resegmenter(), plus convenience setters forresegment_overlap,embedder_pool_size,max_speakers. Validated.build()resolves Mobile/Balanced ONNX throughModelRegistry(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-awareauto()).Pipeline::run()orchestrates: sample-rate guard → M1 powerset segmenter →extract_overlap_time_ranges→ per-primary-chunkapply_overlap_mask(M2 helper) +embedder.embed+l2_normalize→ M3 NME-SC clustering →compute_centroids→ optional M4OverlapResegmenter→ defensive sort →min_speech_secsfilter → legacymerge_segments→DiarizationResult.PipelineError(8 variants); typedConfigErrorfor builder validation.New Cargo feature
pipeline_v2(default-on, requiresonnx + 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. Legacypolyvoice::Pipelineunchanged.Test plan
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 intests/pipeline_v2_e2e_test.rs(Balanced profile, real ONNX viaModelRegistry, single VoxConverse-test WAV)cargo clippy --all-features --tests -- -D warningscleancargo fmt --checkcleancargo check --target wasm32-unknown-unknown --no-default-features --features resegmentation --libclean (pipeline_v2 gated out automatically)scripts/release-gate.shexit 2 (PASS 5, FAIL 0, PENDING 9 — all pending items are M6b/M8/M9)Code review findings (Task 5 fixups merged into branch)
primary_segments, causing later label-by-index zip to assign wrong speakers to every segment after the skip. Fixed by tracking parallelvalid_segmentsVec.apply_overlap_maskon every primary chunk so overlap voices don't contaminate primary speaker embeddings. Fixed.build_overlap_inputsfallback now picks nearest primary turn by midpoint distance.total_cmpsort beforemerge_segmentsso anyResegmenterimpl that returns unsorted output is handled.Out of scope (M6b)
src/pipeline.rs,src/offline.rs,OfflineDiarizer,DiarizationConfig,VadConfig,EnergyVad,VoiceActivityDetector,segment_speech,DummyExtractor,OnnxEmbeddingExtractor,compute_fbankprivatization.pipeline_v2 → pipeline,PipelineV2 → Pipeline.polyvoice diarize --profile mobile|balancedrewrite.src/ffi.rs).python/src/lib.rs).polyvoice-benchrewrite.tests/der_baseline.json.docs/MIGRATING-FROM-0.5.md.OnlineDiarizerdeprecation annotation.References
docs/superpowers/specs/2026-05-07-m6a-pipeline-v2-design.mddocs/superpowers/plans/2026-05-07-m6a-pipeline-v2-plan.mddocs/superpowers/specs/2026-05-07-perfect-diarization-roadmap-v1-design.md§3.1, §5.2, §5.4, §10.1m6a-complete🤖 Generated with Claude Code