Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
1e076a8
split transcribe.cpp into library-level and ASR dispatchers
cjpais Oct 3, 2026
f587fe1
roles: SessionCore base, model role bits, load-time role validation
cjpais Oct 3, 2026
c57038d
cli: split transcribe-cli into main.cpp + asr.cpp
cjpais Oct 3, 2026
fdd1a19
host thread pools: join on every path, return worker exceptions
cjpais Oct 3, 2026
2013849
reject non-finite PCM on every ASR entry point
cjpais Oct 3, 2026
bc2e051
rust: characterize session compute rules
cjpais Oct 3, 2026
6e1bbf6
rust: route session compute through with_compute
cjpais Oct 3, 2026
d8ffa35
swift: characterize session compute rules
cjpais Oct 3, 2026
310ec76
swift: route session compute through withCompute
cjpais Oct 3, 2026
a800604
ts: characterize session compute rules
cjpais Oct 3, 2026
e50ded0
ts: route session compute through #exclusive
cjpais Oct 3, 2026
8274003
ts: keep the stream lease when a feed is rejected
cjpais Oct 3, 2026
f95bb5f
python: characterize session compute rules
cjpais Oct 3, 2026
2c337e7
python: model-wide compute lock
cjpais Oct 3, 2026
f87bcaa
stream: a throwing family hook ends the stream
cjpais Oct 3, 2026
78d3782
python: model-wide stream lease
cjpais Oct 3, 2026
e2752c2
roles: public transcribe_role, transcribe_model_roles, UNSUPPORTED_ROLE
cjpais Oct 3, 2026
b560905
ts: map TRANSCRIBE_ERR_UNSUPPORTED_ROLE
cjpais Oct 3, 2026
abbf9fc
swift: map TRANSCRIBE_ERR_UNSUPPORTED_ROLE, pin header hash
cjpais Oct 3, 2026
3cbdb7f
python: map TRANSCRIBE_ERR_UNSUPPORTED_ROLE
cjpais Oct 3, 2026
d81e04c
rust: map TRANSCRIBE_ERR_UNSUPPORTED_ROLE
cjpais Oct 3, 2026
0ad04b8
roles: DIARIZE role (run only); Sortformer serves it alongside ASR
cjpais Oct 3, 2026
1fd3c5a
cli: diarize path
cjpais Oct 3, 2026
c6bb5ab
bindings: regenerate low-level layers for diarize.h
cjpais Oct 3, 2026
1f3e7bc
roles: catalog role field, validate.py on the diarize API, docs
cjpais Oct 3, 2026
4a2c82b
rust: DIARIZE role wrappers
cjpais Oct 3, 2026
9eabeeb
swift: DIARIZE role wrappers
cjpais Oct 3, 2026
8599661
python: DIARIZE role wrappers
cjpais Oct 3, 2026
e832e33
ts: DIARIZE role wrappers
cjpais Oct 3, 2026
f562481
docs: bindings contract for roles and the DIARIZE session
cjpais Oct 3, 2026
4535564
roles: break: Sortformer is DIARIZE-only; retire SFST
cjpais Oct 3, 2026
3387785
swift: Sortformer is DIARIZE-only
cjpais Oct 3, 2026
afa6f33
rust: Sortformer is DIARIZE-only
cjpais Oct 3, 2026
6ce4903
ts: Sortformer is DIARIZE-only
cjpais Oct 3, 2026
37233ee
python: Sortformer is DIARIZE-only
cjpais Oct 3, 2026
f79d562
c++: trim code and comments from review
cjpais Oct 3, 2026
a0efe19
swift: trim code and tests from review
cjpais Oct 3, 2026
e7cd28c
rust: trim code and tests from review
cjpais Oct 3, 2026
a0172eb
tests, cli, docs: trim from review
cjpais Oct 3, 2026
df25d3d
ts: abort-hook cleanup survives dispose during a run
cjpais Oct 3, 2026
457392a
ts: trim code and tests from review
cjpais Oct 3, 2026
4156b7f
python: trim code and tests from review
cjpais Oct 3, 2026
f2d39ef
review fixes: CI sortformer canary, deterministic concurrency test, r…
cjpais Oct 4, 2026
2165ed3
tests: trim C unit tests to the behavior this PR adds
cjpais Oct 4, 2026
d43e279
rust: trim tests to the binding's own surface
cjpais Oct 4, 2026
a1cca90
python: trim compute-lock and diarize tests
cjpais Oct 4, 2026
64f1387
swift: drop ComputeRulesTests, keep the Busy checks
cjpais Oct 4, 2026
4723ae9
ts: drop compute-rules.test.mjs, keep the two lease fixes
cjpais Oct 4, 2026
cb7145a
review
cjpais Oct 4, 2026
6c6581d
Merge branch 'main' into roles/pr1-diarize
cjpais Oct 4, 2026
ae03b68
review fixes: Python close race, TS param leak, diarize CLI --repeat …
cjpais Oct 5, 2026
f846d70
review fixes
cjpais Oct 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .claude/skills/porting-1-intake/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ field semantics.

2. **Architecture pattern.** One of `encoder-transducer`, `encoder-decoder`, `audio-llm`, `encoder-ctc`. The script's `config.architecture_candidates` is a heuristic starting point. If it doesn't fit, propose a new pattern and have the user accept it based on your research of the architecture.

**Role.** ASR (the product is a transcript) or DIARIZE (the product is
who spoke when; see `docs/roles.md`). A DIARIZE port implements
`DiarizeOps` (`src/transcribe-diarize.h`) instead of the ASR hooks, sets
`"role": "diarize"` in its catalog record, and is accepted on DER
(`scripts/diar/`), not WER.

3. **Acceptance dataset.** Default: LibriSpeech test-clean. Capture any
publisher-reported score in `upstream_benchmarks` when available for
context, but downstream gates use the measured Oracle reference
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/porting-7-wer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ WER progress:

### Step 1: Acceptance manifest (execute or ask-point)

Read the acceptance dataset and metric from `upstream_benchmarks[0].{dataset, metric}`. Slugify: lowercase, spaces → hyphens. `"LibriSpeech test-clean"` → `librispeech-test-clean`. Resolve to `$MANIFEST`; every later step uses `$MANIFEST`, never a reconstructed path. Confirm `metric` is `wer` or `cer`; anything else is out of scope. Do not use any publisher-reported score for pass/fail; the measured Oracle reference score is the gate target.
Read the acceptance dataset and metric from `upstream_benchmarks[0].{dataset, metric}`. Slugify: lowercase, spaces → hyphens. `"LibriSpeech test-clean"` → `librispeech-test-clean`. Resolve to `$MANIFEST`; every later step uses `$MANIFEST`, never a reconstructed path. Confirm `metric` is `wer` or `cer` (`der` for a DIARIZE-role model: score with `scripts/diar/` instead; anything else is out of scope). Do not use any publisher-reported score for pass/fail; the measured Oracle reference score is the gate target.

If the intake's dataset is not covered by `scripts/wer/ingest.py`, extend
that script before running this step.
Expand Down
12 changes: 10 additions & 2 deletions .github/actions/fetch-canary/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ description: >
secret): the model tests then skip, exactly as before this action existed.
Always fetches ALL CI canaries (whisper-tiny + moonshine-streaming ~95 MB,
Canary+SenseVoice text-control canaries ~290 MB, plus the two parakeet 0.6b
family-extension canaries — cache-aware + buffered — at Q4_K_M ~0.95 GB)
family-extension canaries — cache-aware + buffered — at Q4_K_M ~0.95 GB,
plus the Sortformer DIARIZE canary at Q8_0 ~140 MB)
under one cache key — a per-consumer subset would let one job save the shared
key with only its subset in it, and every other consumer would re-download
forever (exact-key hits are never re-saved), which is also why the key carries
Expand Down Expand Up @@ -35,7 +36,7 @@ runs:
uses: actions/cache@v5
with:
path: canary
key: canary-models-v3
key: canary-models-v4
- name: Fetch canary models (cache miss only)
if: inputs.hf-token != ''
shell: bash
Expand Down Expand Up @@ -67,6 +68,12 @@ runs:
uvx --from huggingface_hub hf download \
handy-computer/parakeet-unified-en-0.6b-gguf \
parakeet-unified-en-0.6b-Q4_K_M.gguf --local-dir canary
# Sortformer DIARIZE canary (Q8_0, ~140 MB) for the binding diarize
# tests.
[ -f canary/diar_streaming_sortformer_4spk-v2.1-Q8_0.gguf ] || \
uvx --from huggingface_hub hf download \
handy-computer/diar_streaming_sortformer_4spk-v2.1-gguf \
diar_streaming_sortformer_4spk-v2.1-Q8_0.gguf --local-dir canary
# GITHUB_WORKSPACE (not bash's $PWD) so Windows exports a path
# Python can open rather than an MSYS one.
prefix='${{ inputs.model-path-prefix }}'
Expand All @@ -77,3 +84,4 @@ runs:
echo "TRANSCRIBE_SMOKE_ITN_MODEL=$prefix/canary/SenseVoiceSmall-Q4_K_M.gguf" >> "$GITHUB_ENV"
echo "TRANSCRIBE_SMOKE_PARAKEET_STREAM_MODEL=$prefix/canary/nemotron-speech-streaming-en-0.6b-Q4_K_M.gguf" >> "$GITHUB_ENV"
echo "TRANSCRIBE_SMOKE_PARAKEET_BUFFERED_MODEL=$prefix/canary/parakeet-unified-en-0.6b-Q4_K_M.gguf" >> "$GITHUB_ENV"
echo "TRANSCRIBE_SMOKE_SORTFORMER_MODEL=$prefix/canary/diar_streaming_sortformer_4spk-v2.1-Q8_0.gguf" >> "$GITHUB_ENV"
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ scripts/ci/clang-format.sh --check # verify, no changes
No C++ exception may escape a public entry point.

- A new public entry point must either route through an `api_guard_*`
wrapper (`src/transcribe.cpp`) or be nothrow by construction. Device and
wrapper (`src/transcribe-api-guard.h`) or be nothrow by construction. Device and
registry queries are not pure reads; guard them.
- Entry points with ownership out-params enforce "non-OK => `*out == NULL`,
nothing leaked" in their forwarders on every error return.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ C/C++ speech-to-text inference library. Runs diverse STT model families via [GGU

**Speaker diarization models** (no transcription; verified by DER/JER rather than WER):

<!-- catalog:family-index transcribe=false -->
<!-- catalog:family-index role=diarize -->
| Family | Variants | Available capabilities | Docs |
| --- | --- | --- | --- |
| Streaming Sortformer Diarizer 4spk v2.1 | `diar_streaming_sortformer_4spk-v2.1` | diarize, streaming | [docs/models/diar_streaming_sortformer_4spk-v2.1.md](docs/models/diar_streaming_sortformer_4spk-v2.1.md) |
Expand Down
25 changes: 22 additions & 3 deletions bindings/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,24 @@ Long transcriptions can be cancelled from another thread with
`session.cancel()` — the run raises `Aborted` with the partial transcript on
`exc.partial_result` (same for `OutputTruncated`).

### Diarization

Models whose `model.roles` include `Role.DIARIZE` (Sortformer) answer "who
spoke when" through a diarize session. `run()` returns `SpeakerSegment`
rows (`speaker_id` in `1..model.diarize_info.max_speakers`), grouped by
speaker. Locking, `Busy`, `cancel()` and `close()` work as on `Session`. A
model without the role raises `UnsupportedRole`, as `model.session()` and
`model.capabilities` do on a model without `Role.ASR` (Sortformer serves only
`Role.DIARIZE`).

```python
with model.diarize_session() as diarizer:
turns = diarizer.run(pcm, family=transcribe_cpp.SortformerDiarizeOptions(
preset="very_high_latency"))
for turn in turns:
print(turn.speaker_id, turn.t0_ms, turn.t1_ms)
```

## Backends

`Model(backend=...)` applies a backend policy (`"auto"` uses the best
Expand Down Expand Up @@ -120,9 +138,10 @@ TRANSCRIBE_LIBRARY=../../build-shared/src/libtranscribe.dylib \

## Notes

- One run/stream at a time per `Model` in 0.x: sessions share the model's
compute backend, so serialize runs across sessions (or load one model per
worker). See the `Model` docstring.
- One compute call at a time per `Model`: the binding serializes calls across
all sessions of a model with a model-wide lock (load one model per worker
for parallelism). While a stream is active, other runs and stream begins on
that model raise `transcribe_cpp.Busy`. See the `Model` docstring.
- Import package: `transcribe_cpp`
- Distribution: `transcribe-cpp`
- License: MIT
Loading
Loading