Skip to content
Open
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
31 changes: 31 additions & 0 deletions .github/workflows/pre-commit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Pre-commit

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read

jobs:
pre-commit:
name: Pre-commit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- uses: actions/cache@v4
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
- name: Install pre-commit
run: python -m pip install pre-commit
# Tests already run in the "Test and Build" workflow, so skip the local-only pytest hook here.
- name: Run pre-commit on all files
run: pre-commit run --all-files --show-diff-on-failure
env:
SKIP: pytest-check
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ repos:
- id: requirements-txt-fixer

- repo: https://github.com/psf/black
rev: 23.12.1
rev: 26.5.1
hooks:
- id: black
name: black
files: ^sdk/.*/speechmatics/
args: [--line-length=120, --target-version=py310]

- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.1.9
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.8
hooks:
- id: ruff
name: ruff
Expand Down
40 changes: 25 additions & 15 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Makefile for Speechmatics Python SDKs

.PHONY: help
.PHONY: help check-tools
.PHONY: test-all test-rt test-batch test-agent-stt test-tts test-voice
.PHONY: format-all format-rt format-batch format-agent-stt format-flow format-tts format-voice
.PHONY: lint-all lint-rt lint-batch lint-agent-stt lint-flow lint-tts lint-voice
Expand All @@ -9,6 +9,16 @@
.PHONY: clean-all clean-rt clean-batch clean-agent-stt clean-flow clean-tts clean-voice
.PHONY: install-dev install-dev-rt install-dev-batch install-dev-agent-stt install-dev-flow install-dev-tts install-dev-voice

# Formatter/linter versions are pinned to keep local runs, pre-commit and CI identical.
# When bumping, update: these two variables, the dev extras in sdk/*/pyproject.toml,
# and the `rev:` entries for black and ruff in .pre-commit-config.yaml.
RUFF_VERSION := 0.16.8
BLACK_VERSION := 26.5.1

check-tools:
@ruff --version | grep -qF "$(RUFF_VERSION)" || { echo "Expected ruff $(RUFF_VERSION) but found: $$(ruff --version 2>&1). Run: pip install 'ruff==$(RUFF_VERSION)' 'black==$(BLACK_VERSION)' (or activate the project .venv)"; exit 1; }
@black --version | grep -qF "$(BLACK_VERSION)" || { echo "Expected black $(BLACK_VERSION) but found: $$(black --version 2>&1 | head -1). Run: pip install 'ruff==$(RUFF_VERSION)' 'black==$(BLACK_VERSION)' (or activate the project .venv)"; exit 1; }


help:
@echo "Available commands:"
Expand Down Expand Up @@ -87,57 +97,57 @@ test-voice:
# Formatting targets
format-all: format-rt format-batch format-agent-stt format-flow format-tts format-voice format-tests format-examples

format-rt:
format-rt: check-tools
cd sdk/rt/speechmatics && black .
cd sdk/rt/speechmatics && ruff check --fix .

format-batch:
format-batch: check-tools
cd sdk/batch/speechmatics && black .
cd sdk/batch/speechmatics && ruff check --fix .

format-agent-stt:
format-agent-stt: check-tools
cd sdk/agent_stt/speechmatics && black .
cd sdk/agent_stt/speechmatics && ruff check --fix .

format-flow:
format-flow: check-tools
cd sdk/flow/speechmatics && black .
cd sdk/flow/speechmatics && ruff check --fix .

format-tts:
format-tts: check-tools
cd sdk/tts/speechmatics && black .
cd sdk/tts/speechmatics && ruff check --fix .

format-voice:
format-voice: check-tools
cd sdk/voice/speechmatics && black .
cd sdk/voice/speechmatics && ruff check --fix .

format-tests:
format-tests: check-tools
cd tests && black .
cd tests && ruff check --fix .

format-examples:
format-examples: check-tools
cd examples && black .
cd examples && ruff check --fix .

# Linting targets
lint-all: lint-rt lint-batch lint-agent-stt lint-flow lint-tts lint-voice

lint-rt:
lint-rt: check-tools
cd sdk/rt/speechmatics && ruff check .

lint-batch:
lint-batch: check-tools
cd sdk/batch/speechmatics && ruff check .

lint-agent-stt:
lint-agent-stt: check-tools
cd sdk/agent_stt/speechmatics && ruff check .

lint-flow:
lint-flow: check-tools
cd sdk/flow/speechmatics && ruff check .

lint-tts:
lint-tts: check-tools
cd sdk/tts/speechmatics && ruff check .

lint-voice:
lint-voice: check-tools
cd sdk/voice/speechmatics && ruff check .

# Type checking targets
Expand Down
19 changes: 8 additions & 11 deletions examples/rt/async/microphone/main.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import asyncio
import logging

from speechmatics.rt import (
AsyncClient,
AudioEncoding,
AudioFormat,
Microphone,
OperatingPoint,
ServerMessageType,
TranscriptResult,
TranscriptionConfig,
)

from speechmatics.rt import AsyncClient
from speechmatics.rt import AudioEncoding
from speechmatics.rt import AudioFormat
from speechmatics.rt import Microphone
from speechmatics.rt import OperatingPoint
from speechmatics.rt import ServerMessageType
from speechmatics.rt import TranscriptionConfig
from speechmatics.rt import TranscriptResult

logging.basicConfig(level=logging.INFO)

Expand Down
45 changes: 24 additions & 21 deletions examples/rt/async/multi_chan/main.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
import asyncio
import logging
from contextlib import ExitStack

from speechmatics.rt import AsyncMultiChannelClient, ServerMessageType
from speechmatics.rt import AsyncMultiChannelClient
from speechmatics.rt import ServerMessageType

logging.basicConfig(level=logging.INFO)


async def main():
sources = {
"channel_one": open("channel_one.pcm", "rb"),
"channel_two": open("channel_two.pcm", "rb"),
}

try:
async with AsyncMultiChannelClient() as client:
@client.on(ServerMessageType.ADD_TRANSCRIPT)
def handle_transcript(msg):
channel = msg.get('channel')
transcript = msg['metadata']['transcript']
print(f"[{channel}]: {transcript}")

await client.transcribe(sources)
except Exception as e:
print(f"Transcription error: {e}")
finally:
# Ensure all files are closed
for source in sources.values():
source.close()
# ExitStack guarantees every opened file is closed, even if a later open() fails
with ExitStack() as stack:
sources = {
"channel_one": stack.enter_context(open("channel_one.pcm", "rb")),
"channel_two": stack.enter_context(open("channel_two.pcm", "rb")),
}

try:
async with AsyncMultiChannelClient() as client:

@client.on(ServerMessageType.ADD_TRANSCRIPT)
def handle_transcript(msg):
channel = msg.get("channel")
transcript = msg["metadata"]["transcript"]
print(f"[{channel}]: {transcript}")

await client.transcribe(sources)
except Exception as e:
print(f"Transcription error: {e}")


asyncio.run(main())
35 changes: 16 additions & 19 deletions examples/tts/tts_async_example.py
Original file line number Diff line number Diff line change
@@ -1,55 +1,52 @@
import os
import asyncio

import wave
import wave
from pathlib import Path

from speechmatics.tts import AsyncClient, Voice, OutputFormat
from speechmatics.tts import AsyncClient
from speechmatics.tts import OutputFormat
from speechmatics.tts import Voice


# Set configuration
# Set configuration
TEXT = "Welcome to the future of audio generation from text!"
VOICE = Voice.SARAH
OUTPUT_FORMAT = OutputFormat.RAW_PCM_16000
OUTPUT_FILE = "output.wav"

# Set Format Parameters for WAV output file
SAMPLE_RATE = 16000 #Hz
SAMPLE_WIDTH = 2 # 16-bit audio
CHANNELS = 1 # Mono audio
SAMPLE_RATE = 16000 # Hz
SAMPLE_WIDTH = 2 # 16-bit audio
CHANNELS = 1 # Mono audio


# Save audio to WAV file
async def save_audio_to_wav(audio_data: bytes,
output_file_name: str) -> None:
async def save_audio_to_wav(audio_data: bytes, output_file_name: str) -> None:
with wave.open(output_file_name, "wb") as wav_file:
wav_file.setnchannels(CHANNELS)
wav_file.setsampwidth(SAMPLE_WIDTH)
wav_file.setframerate(SAMPLE_RATE)
wav_file.writeframes(audio_data)


# Generate speech from text and save to WAV file
async def main():
print(f"Generating speech from text: {TEXT}")

try:
async with AsyncClient() as client:
async with await client.generate(
text=TEXT,
voice=VOICE,
output_format=OUTPUT_FORMAT
) as response:
async with await client.generate(text=TEXT, voice=VOICE, output_format=OUTPUT_FORMAT) as response:
# Process the response in chunks and save to WAV
audio_chunks = []
async for chunk in response.content.iter_chunked(1024):
audio_chunks.append(chunk)

# Combine chunks and save to WAV
audio_data = b''.join(audio_chunks)
audio_data = b"".join(audio_chunks)
await save_audio_to_wav(audio_data, OUTPUT_FILE)
print(f"Speech saved to {Path(OUTPUT_FILE).resolve()}")
except Exception as e:
print(f"An error occurred: {e}")


# Run the async main function
if __name__ == "__main__":
asyncio.run(main())
18 changes: 9 additions & 9 deletions examples/tts/tts_autoplay/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@ You must have an audio output device configured on their system for this example
## How it Works

There are two main components in this example, an audio generator and an audio player. These components are run concurrently using asyncio as tasks, ochestrated by the main() function, to generate and play audio in real-time.
### audio_generator()
### audio_generator()

This producer function connects to the Speechmatics TTS API using the AsyncClient. It calls client.generate() with your text, the voice you want to use, and the output format - RAW_PCM_16000 in this example.
The code iterates over the audio data as it is streamed in chunks (iter_chunked), and accumulates in a bytearray buffer.
This producer function connects to the Speechmatics TTS API using the AsyncClient. It calls client.generate() with your text, the voice you want to use, and the output format - RAW_PCM_16000 in this example.
The code iterates over the audio data as it is streamed in chunks (iter_chunked), and accumulates in a bytearray buffer.
The while len(buffer) >= 2 loop reads each audio sample containing 2 bytes, from the buffer, and converts it to a numpy array of int-16 values, which is then put into the audio_queue.
The processed 2 byte sample is then removed from the front of the buffer.
The processed 2 byte sample is then removed from the front of the buffer.
END_OF_STREAM is used as a sentinel value to signal the end of the audio stream, with no more audio data to process.
If an error occurs during audio generation, the END_OF_STREAM sentinel value is still put into the queue to signal the end of the audio stream to prevent the consumer, audio_player(), from getting stuck in an infinite loop, and raises the exception.
### audio_player()
### audio_player()

This consumer function initialises a sounddevice OutputStream, which is responsible for streaming the audio data to the default audio output device. Within the outputstream, the while True loop means there is continous processing of the incoming audio data.
sample = await asyncio.wait_for(play_queue.get(), timeout=0.1) fetches the next sample from the queue, or waits for 0.1 seconds if the queue is empty.
If the sample is END_OF_STREAM, the while loop breaks and the audio player exits.
This consumer function initialises a sounddevice OutputStream, which is responsible for streaming the audio data to the default audio output device. Within the outputstream, the while True loop means there is continous processing of the incoming audio data.
sample = await asyncio.wait_for(play_queue.get(), timeout=0.1) fetches the next sample from the queue, or waits for 0.1 seconds if the queue is empty.
If the sample is END_OF_STREAM, the while loop breaks and the audio player exits.
If the sample is not END_OF_STREAM, it is converted to a numpy array of int-16 values and written to the audio output device using the sounddevice OutputStream.
play_queue.task_done() is called to signal that the sample has been processed.
play_queue.task_done() is called to signal that the sample has been processed.
If an error occurs during audio playback, the END_OF_STREAM sentinel value is still put into the queue to signal the end of the audio stream to prevent the audio_player() from getting stuck in an infinite loop, and raises the exception.

## Installation
Expand Down
2 changes: 1 addition & 1 deletion examples/tts/tts_autoplay/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
sounddevice>=0.4.6
numpy>=1.24.3
sounddevice>=0.4.6
speechmatics-tts>=0.1.0
Loading
Loading