Skip to content

Commit f27ef8e

Browse files
committed
Release RealtimeSTT 1.1.2 early RMS preview
1 parent 50e35ec commit f27ef8e

11 files changed

Lines changed: 282 additions & 23 deletions

.github/workflows/release-checks.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ jobs:
4242
name: build and clean distribution smoke
4343
runs-on: ubuntu-latest
4444
env:
45-
PACKAGE_VERSION: "1.1.1"
45+
PACKAGE_VERSION: "1.1.2"
4646
steps:
4747
- uses: actions/checkout@v4
4848
- uses: actions/setup-python@v5

RELEASE_NOTES.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,32 @@
11
# Release Notes
22

3+
## 1.1.2 - 2026-08-30
4+
5+
This patch release reduces advisory early-RMS Preview latency while preserving
6+
the authoritative transcription retry and full-turn accuracy contract. See
7+
[the release-readiness checklist](docs/release-1.1.2-readiness.md) for the
8+
required build, deployment, attestation, and publication evidence.
9+
10+
### Added
11+
12+
- Production capabilities now advertise the exact `preview.earlyRms` contract:
13+
request mode, 25 ms first-attempt decoder flush, one-attempt limit, and no
14+
advisory empty-result retry.
15+
16+
### Changed
17+
18+
- The exact advisory `previewMode = "early_rms"` appends 25 ms of zero PCM to
19+
its first and only decode and publishes an empty result immediately.
20+
- Authoritative, missing, unknown, and malformed Preview modes retain the
21+
existing conditional 500 ms empty-result retry unchanged.
22+
23+
### Release boundaries
24+
25+
- Miep decides when an early browser RMS pause is advisory; RealtimeSTT does
26+
not change endpoint detection, transcript authority, or response text.
27+
- LLM admission-proxy cancellation is a separate deployment component and is
28+
not part of this Python distribution.
29+
330
## 1.1.1 - 2026-08-28
431

532
This patch release fixes stale Preview ASR work delaying newer full-turn

RealtimeSTT/install_sherpa_models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ def _open_download(
268268
opener: Optional[_Urlopen],
269269
):
270270
offset = partial.stat().st_size if partial.exists() else 0
271-
headers = {"User-Agent": "RealtimeSTT/%s sherpa-model-installer" % "1.1.1"}
271+
headers = {"User-Agent": "RealtimeSTT/%s sherpa-model-installer" % "1.1.2"}
272272
if offset:
273273
headers["Range"] = "bytes=%d-" % offset
274274
request = Request(manifest.archive_url, headers=headers)

RealtimeSTT_server/PRODUCTION_SERVER.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,13 @@ At Preview request time, the complete buffered turn is transcribed immediately.
119119
After a correlated Resume, the complete retained logical turn is transcribed
120120
again; the candidate boundary fences request ownership but does not truncate
121121
the model input. Accurate live text is diagnostic context only; it neither
122-
shortens the Preview input nor replaces the Preview model result. If the first
123-
Preview transcription is empty, the
124-
server appends 500 ms of zero PCM and retries exactly once. A still-empty retry
122+
shortens the Preview input nor replaces the Preview model result. If an
123+
authoritative Preview transcription is empty, the server appends 500 ms of zero
124+
PCM and retries exactly once. Missing, unknown, and malformed `previewMode`
125+
values are authoritative. The exact advisory `previewMode = "early_rms"` adds a
126+
25 ms zero-PCM decoder flush to its first and only attempt, skips that second
127+
decode, and publishes a still-empty result immediately; endpoint and
128+
failure-recovery requests remain authoritative. A still-empty authoritative retry
125129
publishes `status = "empty"`; a model failure publishes `status = "error"`.
126130
Neither path substitutes `liveText` as Preview text. Preview never waits for
127131
either live worker and never uses `mergedText` or `ultrafastSuffix` as its
@@ -153,7 +157,9 @@ short `/v1/...` aliases:
153157
* `GET /api/v1/ready` returns `503` until shared model workers are ready and
154158
healthy.
155159
* `GET /api/v1/capabilities` reports final/live providers and models, active
156-
languages, PCM format/sample rates, limits, and operations.
160+
languages, PCM format/sample rates, limits, and operations. The
161+
`preview.earlyRms` contract advertises the exact advisory request mode,
162+
first-attempt decoder-flush silence, attempt limit, and empty-retry policy.
157163

158164
`GET /health` remains available for HTTP ASR probes and includes the familiar
159165
`engine`, `model`, `device`, `provider`, `compute_type`, `ready`, and warmup

RealtimeSTT_server/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ This directory contains the server and client implementations for the RealtimeST
3535
Use Python 3.11 or 3.12. Install the stable server package with:
3636

3737
```bash
38-
python -m pip install "RealtimeSTT[server]==1.1.1"
38+
python -m pip install "RealtimeSTT[server]==1.1.2"
3939
```
4040

4141
## Server Usage

RealtimeSTT_server/production_server.py

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
API_VERSION = "v1"
4949
PROTOCOL_VERSION = "realtimestt.remote.v1"
5050
SERVER_NAME = "RealtimeSTT production server"
51-
_PACKAGE_VERSION_FALLBACK = "1.1.1"
51+
_PACKAGE_VERSION_FALLBACK = "1.1.2"
5252

5353

5454
def _package_version() -> str:
@@ -66,6 +66,7 @@ def _package_version() -> str:
6666
PCM_FORMAT = "pcm_s16le"
6767
PREVIEW_TAIL_SECONDS = 5.0
6868
PREVIEW_EMPTY_RETRY_SILENCE_SECONDS = 0.5
69+
PREVIEW_EARLY_RMS_FLUSH_SILENCE_SECONDS = 0.025
6970
LATE_FINAL_OPERATION = "late_full_turn_correction"
7071
RESUME_ACK_TYPE = "resume_ack"
7172
_LIVE_CANCEL = object()
@@ -514,6 +515,18 @@ def capabilities_for(settings: ProductionServerSettings) -> Dict[str, Any]:
514515
# realtime is retained as a descriptive alias for live/partial.
515516
"realtime": dict(live_contract),
516517
},
518+
"preview": {
519+
"inputCoverage": "full_turn",
520+
"earlyRms": {
521+
"supported": True,
522+
"requestMode": "early_rms",
523+
"firstAttemptSilenceMs": (
524+
PREVIEW_EARLY_RMS_FLUSH_SILENCE_SECONDS * 1000.0
525+
),
526+
"maxAttempts": 1,
527+
"emptyRetry": False,
528+
},
529+
},
517530
"resume": {
518531
"command": "resume",
519532
"ackType": RESUME_ACK_TYPE,
@@ -2913,6 +2926,7 @@ def _start_preview_worker(
29132926
candidate_base_text: str,
29142927
resume_request_id: Optional[str],
29152928
resume_epoch: int,
2929+
preview_mode: str,
29162930
preview_epoch: int,
29172931
cancelled: threading.Event,
29182932
) -> None:
@@ -2936,6 +2950,7 @@ def _start_preview_worker(
29362950
candidate_base_text,
29372951
resume_request_id,
29382952
resume_epoch,
2953+
preview_mode,
29392954
preview_epoch,
29402955
cancelled,
29412956
)
@@ -3024,6 +3039,7 @@ def _run_preview_worker(
30243039
candidate_base_text: str,
30253040
resume_request_id: Optional[str],
30263041
resume_epoch: int,
3042+
preview_mode: str,
30273043
preview_epoch: int,
30283044
cancelled: threading.Event,
30293045
_release_thread: bool = True,
@@ -3043,6 +3059,7 @@ def _run_preview_worker(
30433059
empty_retry_attempted = False
30443060
empty_retry_recovered = False
30453061
empty_retry_error = None
3062+
empty_retry_suppressed_reason = None
30463063
matched = False
30473064
used_fuzzy_match = False
30483065
anchor_length = 0
@@ -3238,16 +3255,37 @@ def run_asr_attempt(
32383255
cancelled,
32393256
):
32403257
return
3258+
first_attempt_audio = audio
3259+
first_attempt_silence_ms = 0.0
3260+
if preview_mode == "early_rms":
3261+
first_attempt_silence_samples = int(
3262+
round(
3263+
PREVIEW_EARLY_RMS_FLUSH_SILENCE_SECONDS
3264+
* SERVER_SAMPLE_RATE
3265+
)
3266+
)
3267+
first_attempt_audio = np.concatenate(
3268+
(
3269+
audio,
3270+
np.zeros(
3271+
first_attempt_silence_samples,
3272+
dtype=np.float32,
3273+
),
3274+
)
3275+
)
3276+
first_attempt_silence_ms = (
3277+
PREVIEW_EARLY_RMS_FLUSH_SILENCE_SECONDS * 1000.0
3278+
)
32413279
tail_text, first_attempt, first_error = run_asr_attempt(
3242-
audio,
3280+
first_attempt_audio,
32433281
attempt_index=1,
3244-
added_silence_ms=0.0,
3282+
added_silence_ms=first_attempt_silence_ms,
32453283
)
32463284
asr_attempts.append(first_attempt)
32473285
if first_error:
32483286
raise RuntimeError(str(first_error))
32493287

3250-
if not tail_text:
3288+
if not tail_text and preview_mode != "early_rms":
32513289
# The native transducer can occasionally return zero
32523290
# tokens when a snapshot ends directly on its final
32533291
# acoustic frame. Give every empty result exactly one
@@ -3300,6 +3338,13 @@ def run_asr_attempt(
33003338
else:
33013339
tail_text = retry_text
33023340
empty_retry_recovered = bool(tail_text)
3341+
elif not tail_text:
3342+
# An eager browser RMS probe is advisory and can be
3343+
# superseded within milliseconds. Do not spend a
3344+
# second native decode plus 500 ms suffix on an empty
3345+
# result; authoritative and legacy Preview requests
3346+
# retain the quality-preserving retry above.
3347+
empty_retry_suppressed_reason = "early_rms"
33033348

33043349
queue_values = [
33053350
float(attempt["queueMs"])
@@ -3403,6 +3448,7 @@ def run_asr_attempt(
34033448
"type": "preview",
34043449
"turnId": turn_id,
34053450
"previewRequestId": request_id,
3451+
"previewMode": preview_mode,
34063452
"text": cumulative_text,
34073453
"cumulativeText": cumulative_text,
34083454
"liveText": cumulative_live_text,
@@ -3483,6 +3529,12 @@ def run_asr_attempt(
34833529
else 0.0
34843530
),
34853531
"emptyRetryError": empty_retry_error,
3532+
"emptyRetrySuppressedReason": empty_retry_suppressed_reason,
3533+
"firstAttemptSilenceMs": (
3534+
PREVIEW_EARLY_RMS_FLUSH_SILENCE_SECONDS * 1000.0
3535+
if preview_mode == "early_rms"
3536+
else 0.0
3537+
),
34863538
"requestToPublishMs": round(
34873539
max(0.0, publish_ready_at - requested_at) * 1000.0,
34883540
3,
@@ -3685,6 +3737,11 @@ async def preview(self, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
36853737
"invalid_preview_request",
36863738
"previewRequestId must be a non-empty string of at most 128 characters",
36873739
)
3740+
preview_mode = (
3741+
"early_rms"
3742+
if payload.get("previewMode") == "early_rms"
3743+
else "authoritative"
3744+
)
36883745
audio_revision = turn.audio_revision
36893746
audio_frames = turn.audio_frames
36903747
turn.preview_requested = True
@@ -3737,6 +3794,7 @@ async def preview(self, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
37373794
candidate_base_text,
37383795
resume_request_id,
37393796
resume_epoch,
3797+
preview_mode,
37403798
preview_epoch,
37413799
cancelled,
37423800
)
@@ -3747,6 +3805,7 @@ async def preview(self, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
37473805
"sessionId": self.session_id,
37483806
"turnId": turn_id,
37493807
"previewRequestId": request_id,
3808+
"previewMode": preview_mode,
37503809
"audioPackets": packet_count,
37513810
"audioDurationSeconds": round(audio_seconds, 6),
37523811
}
@@ -3825,6 +3884,7 @@ async def _finalize_preview_only(self) -> Optional[Dict[str, Any]]:
38253884
candidate_base_text,
38263885
resume_request_id,
38273886
resume_epoch,
3887+
"authoritative",
38283888
preview_epoch,
38293889
preview_cancelled,
38303890
)

docs/release-1.1.2-readiness.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# RealtimeSTT 1.1.2 release readiness
2+
3+
Status date: 2026-08-30
4+
5+
This checklist is evidence-conservative. An unchecked gate is not a release
6+
claim, and this file does not authorize publication.
7+
8+
## Required gates
9+
10+
- [ ] Base the candidate on current public master and review the exact commit,
11+
linked worktrees, branch, tag, and remote divergence.
12+
- [ ] Run the focused early-RMS, authoritative Preview, production-session,
13+
capabilities, package-version, and release-guard tests for the exact commit.
14+
- [ ] Build the wheel and sdist once from a fresh clean worktree and validate
15+
their metadata, contents, filenames, and privacy checks.
16+
- [ ] Install that exact wheel into the declared Linux runtime, verify all three
17+
import roots, restart the service, and run authenticated health,
18+
capabilities, representative Preview, and 25 ms early-RMS acceptance.
19+
- [ ] Attest `RealtimeSTT`, `RealtimeSTT_server`, and
20+
`example_fastapi_server` against the exact deployed wheel and sdist no more
21+
than 30 minutes before publication.
22+
- [ ] Publish only through `tools/release_guard.py publish --repository pypi`,
23+
proving the remote release branch and `v1.1.2` tag resolve to the attested
24+
commit and confirming both uploaded hashes through PyPI.
25+
- [ ] Download the exact published wheel and sdist into fresh environments and
26+
repeat package provenance and smoke checks.
27+
28+
## Known boundaries
29+
30+
The exact advisory `previewMode = "early_rms"` uses one decode with 25 ms of
31+
zero-PCM decoder-flush silence and no empty-result retry. Authoritative,
32+
missing, unknown, and malformed modes retain the quality-preserving conditional
33+
500 ms retry. Miep owns browser RMS timing and the decision to use the advisory
34+
mode. LLM admission-proxy cancellation is outside this distribution.
35+
36+
The pinned Nemotron-live/Parakeet-final profile targets Linux x86-64. Native
37+
Windows remains a development target for this pair. Model weights are external
38+
artifacts and are not shipped in the Python distributions.
39+
40+
## Evidence recording
41+
42+
Exact-commit workflow URLs, deployed import paths, artifact hashes, runtime
43+
smoke results, and PyPI confirmation belong in the matching GitHub release.

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from setuptools.command.build_py import build_py as _build_py
77

88

9-
current_version = "1.1.1"
9+
current_version = "1.1.2"
1010

1111

1212
INSTALL_GUIDE = """

tests/unit/test_install_sherpa_models.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ def test_resume_uses_range_and_commits_atomically(self):
123123

124124
def opener(request, timeout=None):
125125
range_header = request.get_header("Range")
126-
calls.append((range_header, timeout))
126+
user_agent = request.get_header("User-agent")
127+
calls.append((range_header, user_agent, timeout))
127128
if range_header is None:
128129
return _Response(archive[:split], fail_after=split)
129130
self.assertEqual("bytes=%d-" % split, range_header)
@@ -142,6 +143,10 @@ def opener(request, timeout=None):
142143
destination = installer.install_model(manifest, self.root, urlopen_fn=opener)
143144
self.assertTrue((destination / "tokens.txt").is_file())
144145
self.assertEqual(2, len(calls))
146+
self.assertEqual(
147+
{"RealtimeSTT/1.1.2 sherpa-model-installer"},
148+
{call[1] for call in calls},
149+
)
145150
self.assertEqual("bytes=%d-" % split, calls[1][0])
146151
self.assertFalse(partial.exists())
147152
self.assertTrue(

0 commit comments

Comments
 (0)