From 490222576977d200e2b25fcfd5d231865beef757 Mon Sep 17 00:00:00 2001 From: Lorna Armstrong Date: Tue, 15 Sep 2026 15:54:43 +0100 Subject: [PATCH 1/3] Add emit_sentences support --- examples/agent_stt/realtime_file/main.py | 6 +++++ sdk/agent_stt/README.md | 7 ++++++ .../speechmatics/agent_stt/_models.py | 7 ++++++ tests/agent_stt/test_client.py | 25 +++++++++++++++++++ tests/agent_stt/test_config.py | 14 ++++++++--- 5 files changed, 56 insertions(+), 3 deletions(-) diff --git a/examples/agent_stt/realtime_file/main.py b/examples/agent_stt/realtime_file/main.py index eb910dc2..66578445 100644 --- a/examples/agent_stt/realtime_file/main.py +++ b/examples/agent_stt/realtime_file/main.py @@ -43,6 +43,11 @@ def parse_args() -> argparse.Namespace: "--turn-seconds", type=float, default=5.0, help="fake turn length, --turn-detection external only" ) parser.add_argument("--no-partials", action="store_true") + parser.add_argument( + "--emit-sentences", + action="store_true", + help="split multi-sentence segments so each segment carries a single sentence", + ) return parser.parse_args() @@ -75,6 +80,7 @@ def build_client(args: argparse.Namespace, clock: Clock) -> AgentSttAsyncClient: transcription_config = TranscriptionConfig( language=args.language, enable_partials=not args.no_partials, + emit_sentences=args.emit_sentences or None, ) turn_config = TurnConfig(turn_detection_mode=TurnDetectionMode(args.turn_detection)) diff --git a/sdk/agent_stt/README.md b/sdk/agent_stt/README.md index 04b7d538..638b8e97 100644 --- a/sdk/agent_stt/README.md +++ b/sdk/agent_stt/README.md @@ -127,6 +127,13 @@ point, so the transcriber never sees a name it has no notion of. The RT models ( `standard`) are not Agent STT models and are not accepted here; the deprecated `operating_point` still passes through, and suppresses the `model` default so the two never arrive together. +`emit_sentences` splits a multi-sentence segment on its sentence boundaries, so each segment +carries one sentence. Off unless you ask for it, and fixed for the life of the session: + +```python +transcription_config = TranscriptionConfig(language="en", emit_sentences=True) +``` + Engine silence-based end of utterance is not offered here. A turn ends either because the service's VAD said so, or because you called `finalize()`. diff --git a/sdk/agent_stt/speechmatics/agent_stt/_models.py b/sdk/agent_stt/speechmatics/agent_stt/_models.py index aa75f855..cbe713e2 100644 --- a/sdk/agent_stt/speechmatics/agent_stt/_models.py +++ b/sdk/agent_stt/speechmatics/agent_stt/_models.py @@ -185,13 +185,20 @@ class TranscriptionConfig(RTTranscriptionConfig): model: Agent STT model, defaulting to `DEFAULT_MODEL`. The proxy in front of the service resolves the name to the engine's operating point, so a model the transcriber has no notion of still routes correctly. + emit_sentences: Split multi-sentence segments on sentence boundaries, so each segment + carries a single sentence. Fixed for the life of the session. Defaults to False + service-side. Examples: >>> transcription_config = TranscriptionConfig(language="en", enable_partials=True) + + One sentence per segment: + >>> transcription_config = TranscriptionConfig(language="en", emit_sentences=True) """ model: Model = _UNSET additional_vocab: Optional[list[Union[AdditionalVocabEntry, dict[str, Any]]]] = None + emit_sentences: Optional[bool] = None def __post_init__(self) -> None: if self.model is not _UNSET and self.operating_point is not None: # type: ignore[unreachable] diff --git a/tests/agent_stt/test_client.py b/tests/agent_stt/test_client.py index 043aeb2a..3db89463 100644 --- a/tests/agent_stt/test_client.py +++ b/tests/agent_stt/test_client.py @@ -268,6 +268,31 @@ async def test_start_session_turn_config_wins_over_the_constructor(client, monke assert transport.messages[0]["turn_config"] == {"turn_detection_mode": "vad"} +@pytest.mark.asyncio +async def test_emit_sentences_reaches_start_recognition(client, monkeypatch): + """The whole config dict is sent, but assert it rather than trust it: the flag is the session.""" + transport = StubTransport() + client._transport = transport + monkeypatch.setattr(client, "_ws_connect", _noop) + monkeypatch.setattr(client, "_wait_recognition_started", _noop) + + await client.start_session(transcription_config=TranscriptionConfig(language="en", emit_sentences=True)) + + assert transport.messages[0]["transcription_config"]["emit_sentences"] is True + + +@pytest.mark.asyncio +async def test_emit_sentences_absent_when_not_asked_for(client, monkeypatch): + transport = StubTransport() + client._transport = transport + monkeypatch.setattr(client, "_ws_connect", _noop) + monkeypatch.setattr(client, "_wait_recognition_started", _noop) + + await client.start_session(transcription_config=TranscriptionConfig(language="en")) + + assert "emit_sentences" not in transport.messages[0]["transcription_config"] + + def test_session_info_tracks_the_request_id_across_sessions(client): """RT mints a fresh request_id per session; session_info must not keep the first one.""" start_session(client) diff --git a/tests/agent_stt/test_config.py b/tests/agent_stt/test_config.py index e2938adb..b2aa73a4 100644 --- a/tests/agent_stt/test_config.py +++ b/tests/agent_stt/test_config.py @@ -11,9 +11,7 @@ def test_service_vad_is_the_default(): def test_external_mode_selects_external_turn_detection(): - assert TurnConfig(turn_detection_mode=TurnDetectionMode.EXTERNAL).to_dict() == { - "turn_detection_mode": "external" - } + assert TurnConfig(turn_detection_mode=TurnDetectionMode.EXTERNAL).to_dict() == {"turn_detection_mode": "external"} def test_turn_detection_is_not_in_the_transcription_config(): @@ -48,6 +46,16 @@ def test_rt_fields_still_work(): assert result["max_delay"] == 1.5 +def test_emit_sentences_omitted_by_default(): + """Left out so the service applies its own default of false.""" + assert "emit_sentences" not in TranscriptionConfig(language="en").to_dict() + + +@pytest.mark.parametrize("value", [True, False]) +def test_emit_sentences_sent_when_set(value): + assert TranscriptionConfig(language="en", emit_sentences=value).to_dict()["emit_sentences"] is value + + def test_model_and_operating_point_conflict(): with pytest.raises(ValueError): TranscriptionConfig(model=Model.LINDEN_1, operating_point="enhanced") From de557a08b39920f0b31c1c0f06a86169264d1e34 Mon Sep 17 00:00:00 2001 From: Lorna Armstrong Date: Wed, 16 Sep 2026 09:12:36 +0100 Subject: [PATCH 2/3] Fix model comment --- sdk/agent_stt/speechmatics/agent_stt/_models.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sdk/agent_stt/speechmatics/agent_stt/_models.py b/sdk/agent_stt/speechmatics/agent_stt/_models.py index cbe713e2..625b16fc 100644 --- a/sdk/agent_stt/speechmatics/agent_stt/_models.py +++ b/sdk/agent_stt/speechmatics/agent_stt/_models.py @@ -182,9 +182,7 @@ class TranscriptionConfig(RTTranscriptionConfig): Attributes: additional_vocab: Words to bias the engine towards, as `AdditionalVocabEntry` objects or raw dicts. - model: Agent STT model, defaulting to `DEFAULT_MODEL`. The proxy in front of the - service resolves the name to the engine's operating point, so a model the - transcriber has no notion of still routes correctly. + model: Agent STT model to transcribe with. Defaults to `DEFAULT_MODEL`. emit_sentences: Split multi-sentence segments on sentence boundaries, so each segment carries a single sentence. Fixed for the life of the session. Defaults to False service-side. From df73c94f879dcc9b3810c0cef8d07dfea731e6f6 Mon Sep 17 00:00:00 2001 From: Lorna Armstrong Date: Tue, 29 Sep 2026 15:47:05 +0100 Subject: [PATCH 3/3] Improve comments for end of sentence splitting --- examples/agent_stt/realtime_file/main.py | 2 +- sdk/agent_stt/README.md | 5 +++-- sdk/agent_stt/speechmatics/agent_stt/_models.py | 7 ++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/examples/agent_stt/realtime_file/main.py b/examples/agent_stt/realtime_file/main.py index 66578445..07988098 100644 --- a/examples/agent_stt/realtime_file/main.py +++ b/examples/agent_stt/realtime_file/main.py @@ -46,7 +46,7 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--emit-sentences", action="store_true", - help="split multi-sentence segments so each segment carries a single sentence", + help="one sentence per segment, split on end-of-sentence punctuation: 'Hi. How are you?' arrives as two segments", ) return parser.parse_args() diff --git a/sdk/agent_stt/README.md b/sdk/agent_stt/README.md index 638b8e97..2e4e9b8a 100644 --- a/sdk/agent_stt/README.md +++ b/sdk/agent_stt/README.md @@ -127,8 +127,9 @@ point, so the transcriber never sees a name it has no notion of. The RT models ( `standard`) are not Agent STT models and are not accepted here; the deprecated `operating_point` still passes through, and suppresses the `model` default so the two never arrive together. -`emit_sentences` splits a multi-sentence segment on its sentence boundaries, so each segment -carries one sentence. Off unless you ask for it, and fixed for the life of the session: +`emit_sentences` splits a segment after each end-of-sentence punctuation mark (such as `.`, `?` +or `!`), so each segment carries one sentence: "Hi there. How are you?" arrives as two segments +rather than one. Off unless you ask for it, and fixed for the life of the session: ```python transcription_config = TranscriptionConfig(language="en", emit_sentences=True) diff --git a/sdk/agent_stt/speechmatics/agent_stt/_models.py b/sdk/agent_stt/speechmatics/agent_stt/_models.py index 625b16fc..7de5f3ea 100644 --- a/sdk/agent_stt/speechmatics/agent_stt/_models.py +++ b/sdk/agent_stt/speechmatics/agent_stt/_models.py @@ -183,9 +183,10 @@ class TranscriptionConfig(RTTranscriptionConfig): additional_vocab: Words to bias the engine towards, as `AdditionalVocabEntry` objects or raw dicts. model: Agent STT model to transcribe with. Defaults to `DEFAULT_MODEL`. - emit_sentences: Split multi-sentence segments on sentence boundaries, so each segment - carries a single sentence. Fixed for the life of the session. Defaults to False - service-side. + emit_sentences: Split a segment after each end-of-sentence punctuation mark (such as + `.`, `?` or `!`), so each segment carries a single sentence. For example, + "Hi there. How are you?" arrives as two segments rather than one. Fixed for the + life of the session. Defaults to False service-side. Examples: >>> transcription_config = TranscriptionConfig(language="en", enable_partials=True)