Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
101 changes: 101 additions & 0 deletions examples/batch/transcribe_file_sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""
Sync example showing batch transcription with the blocking Client.

No event loop is involved. This also demonstrates synchronous transcription
(the ``wait`` parameter), where the server holds the request open until the
transcript is ready, so one request replaces submit + poll + fetch.
"""

import os

from speechmatics.batch import Client
from speechmatics.batch import FormatType
from speechmatics.batch import JobConfig
from speechmatics.batch import JobStatus
from speechmatics.batch import JobType
from speechmatics.batch import Transcript
from speechmatics.batch import TranscriptionConfig

audio_file = os.getenv("AUDIO_FILE_PATH", os.path.join(os.path.dirname(__file__), "../example1.wav"))

# Seconds to let the server wait for the job before falling back to polling.
wait_seconds = int(os.getenv("WAIT_SECONDS", "60"))


def job_config() -> JobConfig:
return JobConfig(
type=JobType.TRANSCRIPTION,
transcription_config=TranscriptionConfig(language="en", enable_entities=True),
)


def transcribe_in_one_request(client: Client) -> None:
"""Submit and get the transcript back in a single request."""
print("\n1) Synchronous transcription (one request)")
print(f" Submitting {audio_file} with wait={wait_seconds}s")

job = client.submit_job(audio_file, config=job_config(), wait=wait_seconds)
print(f" Job {job.id} came back with status: {job.status.value}")

if job.status == JobStatus.DONE and job.transcript is not None:
print(" The transcript was embedded in the submit response, so no further calls were needed.")
print(f" Transcript: {job.transcript.transcript_text}")
elif job.status == JobStatus.DONE:
# Finished, but the response carried no transcript: one extra call.
print(" Job finished but no transcript was embedded; fetching it separately.")
print(f" Transcript: {client.get_transcript(job.id).transcript_text}")
else:
# The wait elapsed before the job finished, which is expected for longer audio.
# No timeout here: how long transcription takes depends on the audio's
# length, so the example doesn't guess at a deadline.
print(f" Still running after {wait_seconds}s, falling back to polling.")
result = client.wait_for_completion(job.id, polling_interval=2.0, timeout=None)
print(f" Transcript: {result.transcript_text}")


def transcribe_as_plain_text(client: Client) -> None:
"""The same thing in one call, asking for plain text instead of JSON."""
print("\n2) One call via transcribe(), plain text output")

text = client.transcribe(
audio_file,
config=job_config(),
format_type=FormatType.TXT,
wait=wait_seconds,
timeout=None,
)
print(f" Transcript: {text}")


def submit_and_poll(client: Client) -> None:
"""The classic flow, for comparison: submit, then poll until done."""
print("\n3) Classic flow: submit, then poll")

job = client.submit_job(audio_file, config=job_config())
print(f" Job submitted: {job.id}")

result = client.wait_for_completion(job.id, polling_interval=2.0, timeout=None)
if isinstance(result, Transcript):
print(f" Transcript: {result.transcript_text}")
else:
print(f" Transcript: {result}")


def main() -> None:
"""Run sync batch transcription examples."""
with Client(api_key=os.getenv("SPEECHMATICS_API_KEY")) as client:
try:
transcribe_in_one_request(client)
transcribe_as_plain_text(client)
submit_and_poll(client)

except FileNotFoundError:
print(f"Audio file not found: {audio_file}")
print("Set AUDIO_FILE_PATH environment variable to specify audio file")

except Exception as e:
print(f"Transcription failed: {e}")


if __name__ == "__main__":
main()
1 change: 0 additions & 1 deletion sdk/agent_stt/speechmatics/__init__.py

This file was deleted.

236 changes: 208 additions & 28 deletions sdk/batch/MIGRATION.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,63 @@
# Batch SDK Migration Guide

This guide helps users migrate from the legacy Speechmatics Batch Client (`speechmatics-python`) to the new Speechmatics Batch SDK (`speechmatics-batch`). The new SDK provides a modern async API, improved error handling, and enhanced authentication options with minimal dependencies.
This guide helps you migrate from the legacy Speechmatics Batch Client
(`speechmatics-python`, class `BatchClient`) to the new Speechmatics Batch SDK
(`speechmatics-batch`).

**Coming from blocking code?** Use `Client` — it is a drop-in-shaped
replacement for `BatchClient`: no `asyncio` required, same synchronous
mental model. `AsyncClient` exposes identical methods with `async`/`await`,
for code that already runs an event loop, but adopting it is optional.

## Quick reference

| Legacy (`BatchClient`) | New (`Client` / `AsyncClient`) | Notes |
| --- | --- | --- |
| `BatchClient(auth_token)` | `Client(api_key=...)` / `AsyncClient(api_key=...)` | Also reads `SPEECHMATICS_API_KEY` — see [Credentials](#credentials) |
| `client.connect()` / `with BatchClient(...)` | `Client(...)` used directly, or `with Client(...)` | `connect()` no longer exists or is needed |
| `client.submit_job(audio, transcription_config)` → `str` (job ID) | `client.submit_job(audio, config=...)` → `JobDetails` | Return type changed — use `job.id` |
| `client.submit_jobs(paths, config, concurrency=5)` | No direct equivalent | See [Bulk submission](#bulk-submission) |
| `client.check_job_status(job_id)` → raw `dict` | `client.get_job_info(job_id)` → `JobDetails` | |
| `client.get_job_result(job_id, transcription_format="json-v2")` | `client.get_transcript(job_id, format_type=FormatType.JSON)` → raises `TranscriptNotReadyError` if the job isn't done yet | Catch `TranscriptNotReadyError` to retry — see [Exceptions](#exceptions) |
| `client.wait_for_completion(job_id, transcription_format="txt")` | `client.wait_for_completion(job_id, format_type=FormatType.TXT)` | **Default format changed** — see [Breaking Changes](#breaking-changes) |
| `client.delete_job(job_id, force=False)` → `str` | `client.delete_job(job_id, force=False)` → `None`, raises on failure | |
| `client.list_jobs()` → `List[dict]` | `client.list_jobs()` → `List[JobDetails]` | |

## Significant Changes
## Installation

- **Fully async API**: All operations now use async/await pattern with `AsyncClient`
- **Enhanced authentication**: Support for both API key and JWT authentication methods
- **Better error handling**: More specific exceptions and clearer error messages
- **Lightweight package**: Minimal dependencies for faster installation and reduced conflicts
- **Improved job management**: Better job status tracking and result handling
- **Streamlined configuration**: Unified `JobConfig` for all job types
- **URL and API key configuration**: Allows loading URL and API key from environment variables
``` bash
pip install speechmatics-batch
```

### Breaking Changes
## Credentials

- **Import paths**: `speechmatics.batch_client` → `speechmatics.batch`
- **Client class**: `BatchClient` → `AsyncClient`
- **All methods**: Synchronous → Asynchronous (requires `await`)
- **Configuration**: `BatchTranscriptionConfig` → `JobConfig` with `TranscriptionConfig`
- **Job submission**: Direct config parameter → Structured `JobConfig` object
- **Result format**: `transcription_format` parameter → `format_type` parameter
- **Authentication**: API key parameter naming changed to `api_key`
- **CLI not available**: CLI will be released as a separate package
Legacy `BatchClient` took an explicit `auth_token`, or fell back to
`~/.speechmatics/config` (a file written by the legacy CLI's
`speechmatics config set`). The new SDK does not read that file — pass
`api_key` explicitly, or set an environment variable. If you configured the
legacy CLI, your token is already on disk:

## Installation
```bash
cat ~/.speechmatics/config # find the auth_token you already have

``` bash
pip install speechmatics-batch
export SPEECHMATICS_API_KEY="<paste the auth_token value here>"
```

```python
from speechmatics.batch import Client

with Client() as client: # reads SPEECHMATICS_API_KEY
...
```

A custom `ConnectionSettings.url` is replaced by the `SPEECHMATICS_BATCH_URL`
environment variable, or `Client(url=...)` / `ConnectionConfig(url=...)`.

## Usage

Before
### Submit and wait (blocking — the direct replacement)

Before:

```python
from speechmatics.models import ConnectionSettings, BatchTranscriptionConfig
Expand All @@ -45,20 +71,174 @@ with BatchClient("API-KEY") as client:
print(transcript)
```

After
After:

```python
from speechmatics.batch import Client, FormatType, JobConfig, JobType, TranscriptionConfig

with Client(api_key="API-KEY") as client:
config = JobConfig(
type=JobType.TRANSCRIPTION,
transcription_config=TranscriptionConfig(language=LANGUAGE),
)

job = client.submit_job(PATH_TO_FILE, config=config)

transcript = client.wait_for_completion(job.id, format_type=FormatType.TXT)

print(transcript)
```

### Submit and wait (async, if you're adopting `asyncio`)

```python
from speechmatics.batch import AsyncClient, FormatType, JobConfig, JobType, TranscriptionConfig

async with AsyncClient(os.environ.get("SPEECHMATICS_API_KEY")) as client:
async with AsyncClient(api_key="API-KEY") as client:
config = JobConfig(
type=JobType.TRANSCRIPTION,
transcription_config=TranscriptionConfig(language="en"),
transcription_config=TranscriptionConfig(language=LANGUAGE),
)

job = await client.submit_job("audio.wav", config=config)
job = await client.submit_job(PATH_TO_FILE, config=config)

transcript = await client.wait_for_completion(job.id, format_type=FormatType.TXT)

print(transcript)
```

### Bulk submission

`submit_jobs()` has no direct equivalent. Reproduce a concurrency-limited
batch with `asyncio.gather` and a semaphore — see the
[README's Bulk and Concurrent Transcription section](README.md#bulk-and-concurrent-transcription)
for a full example (both async and blocking) that submits each file, waits
for it, and returns its transcript. It defaults to a concurrency of 5,
matching `submit_jobs()`'s old default.

result = await client.wait_for_completion(job.id, format_type=FormatType.TXT)
### Manual job management

print(f"Transcript: {result.transcript_text}")
Before:

```python
status = client.check_job_status(job_id)["job"]["status"]
if status == "done":
result = client.get_job_result(job_id, transcription_format="json-v2")
```

After:

```python
from speechmatics.batch import JobStatus

job_info = client.get_job_info(job_id)
if job_info.status == JobStatus.DONE:
result = client.get_transcript(job_id)
```

## Breaking Changes

- **Import paths**: `speechmatics.batch_client` → `speechmatics.batch`
- **Client class**: `BatchClient` → `AsyncClient`, or `Client` to stay synchronous
- **All methods async on `AsyncClient`** (requires `await`); `Client` keeps them blocking
- **Configuration**: `BatchTranscriptionConfig` → `JobConfig` with `TranscriptionConfig`
- **`submit_job()` return type changed**: it returned a job ID `str`; it now
returns a `JobDetails` object — use `job.id` instead of the return value
directly.
- **`wait_for_completion()` default format changed**: the legacy default was
`transcription_format="txt"`; the new default is `format_type=FormatType.JSON`.
**If your code relied on the default rather than passing the format
explicitly, you will silently start getting JSON instead of plain text.**
- **Result format**: `transcription_format` string parameter → `format_type`,
a `FormatType` enum
- **Authentication**: API key parameter renamed to `api_key`; credentials are
no longer read from `~/.speechmatics/config` — see [Credentials](#credentials)
- **`get_transcript()` raises `TranscriptNotReadyError` when a job isn't
finished yet** — catch this specifically to retry, rather than treating it
the same as a missing job (`JobError`).
- **`submit_jobs()` (bulk submission) removed**: no direct equivalent — see
[Bulk submission](#bulk-submission).
- **No CLI**: there is no command-line tool for `speechmatics-batch`, and
none is planned. Use the SDK directly from a script for one-off jobs.

## Exceptions

| Legacy exception (`speechmatics.exceptions`) | Raised by | New exception (`speechmatics.batch`) |
| --- | --- | --- |
| `JobNotFoundException` | `check_job_status()`, `delete_job()` (job not found) | `JobError` |
| `JobNotFoundException` | `get_job_result()` (job not ready, or not found) | `TranscriptNotReadyError` (from `get_transcript()`) |
| `TranscriptionError` (invalid format string) | `get_job_result()` | N/A — `format_type` takes a `FormatType` enum member, not a string |
| `TranscriptionError` (job in an unexpected state) | `wait_for_completion()` | `JobError` |
| `httpx.HTTPStatusError` (401/403) | any request | `AuthenticationError` |
| *(no equivalent — surfaces as a generic `httpx.HTTPStatusError`)* | job data expired (HTTP 410) | `JobExpiredError` |

## Changes within `speechmatics-batch`

- **Type hints now reach type checkers.** `speechmatics` is a PEP 420 namespace package, so the
`py.typed` marker is found where mypy and pyright look for it. Previously an installed
`speechmatics-batch` was reported as untyped (`missing library stubs or py.typed marker`) and
every call inferred as `Any`. Type checking your code against this SDK may now surface real
errors that were silently ignored before.
- **Invalid credentials now raise `AuthenticationError`**: HTTP 401/403 responses were
previously re-wrapped as `TransportError` before reaching callers, despite the documented
behaviour. Code that catches `TransportError` for authentication failures must catch
`AuthenticationError` instead.
- **Unavailable transcripts now raise `TranscriptNotReadyError`**: `get_transcript()` answers
an HTTP 404 with this exception, which subclasses `JobError`, so existing `except JobError`
handlers keep working.
- **Expired jobs now raise `JobExpiredError`**: Speechmatics retains job data for a limited
time; once it expires, both `get_job_info()` and `get_transcript()` answer with HTTP 410,
which now raises this exception (a subclass of `JobError`) instead of a generic `JobError`
with the raw response embedded in the message.
- **`JobStatus.EXPIRED` removed**: the API signals expiry via HTTP 410 on the endpoints above,
never as a `status` field value, so this member never matched anything a real API response
could produce and has been removed.
- **`AuthBase` methods are no longer abstract**: a custom auth class implements
`get_auth_headers` (async), `get_auth_headers_sync` (blocking), or both. Previously a
blocking-only implementation could not be instantiated because the async method was
abstract. Calling a method a subclass does not implement raises `NotImplementedError`
naming the missing one.
- **`delete_job()` accepts `force`**: the API refuses to delete a running job with HTTP 423, so
deleting one previously was not possible. Pass `force=True` to delete it anyway; without it,
the resulting `JobError` now says so explicitly.
- **New dependency: `httpx`**, alongside the existing `aiohttp`/`aiofiles`. It powers the new
blocking `Client`; `AsyncClient` does not use it.
- **Waiting is now bounded by default**: `wait_for_completion()` and `transcribe()` default to
`timeout=3600.0` instead of waiting forever, so a job that never reaches a terminal state can
no longer hang a thread or task indefinitely. Pass `timeout=None` for the old behaviour.
- **Status polling now backs off**: it starts at `min_polling_interval` (0.5s, a new argument)
and ramps towards `polling_interval`, which is now the ceiling rather than a fixed interval.
Its default is unchanged at 5 seconds, so long jobs poll as often as they did before while
short ones are picked up sooner.
- **Polling intervals must be greater than 0**: zero never backs off, so it polled in an
unthrottled loop. It now raises `ValueError`.
- **A failed status check no longer abandons a running job**: connection errors, request
timeouts and HTTP 408/429/5xx are retried, up to 5 consecutive failures.

## Polling behaviour

Legacy `wait_for_completion()` slept for 10% of the audio's duration before
polling at all, then polled at a fixed 15-second interval regardless of file
length, and gave up after an hour.

The new SDK does not scale the wait to the audio's duration. It starts at
`min_polling_interval` (0.5 seconds) and backs off towards `polling_interval`
(5 seconds), with up to 20% jitter so concurrent clients don't synchronise, so
short jobs are picked up quickly while long jobs settle into infrequent checks.
Waiting is still bounded at an hour by default, matching the legacy cap; pass
`timeout=None` to wait indefinitely.

Two things the legacy client did not do:

- **Transient failures are retried.** A status check that fails with a
connection error, a request timeout or HTTP 408/429/5xx is retried, up to 5
consecutive failures, rather than abandoning a job that is still running.
Failures that are an answer rather than a blip — bad credentials, an unknown
job, an expired job — are still raised immediately.
- **The API may hold each status request open briefly before answering it.**
The SDK does not depend on how long that is, so treat the intervals above as
what the client adds on top, not as the total time between checks.

If you added your own delay logic on top of the legacy client to reduce latency
on short files, you no longer need it — tune `min_polling_interval` and
`polling_interval` on `wait_for_completion()`/`transcribe()` instead.
Loading
Loading