Skip to content

Home Assistant Voice Integration

Give Home Assistant's Assist voice pipeline speech-to-text and text-to-speech backed by Amazon Transcribe and Amazon Polly, through stdapi.ai's OpenAI-compatible audio routes.

At a glance

  • No Cloud Voice Subscription
    Replace a cloud speech-to-text/text-to-speech subscription with Amazon Transcribe and Amazon Polly, billed at Bedrock/AWS rates.

  • No Third-Party Voice Vendor
    Spoken audio and transcripts are processed by Amazon Transcribe, Polly and Bedrock in the regions you enable, reached through your own deployment — no consumer voice-assistant vendor in the path.

  • Works with Your Existing Assist Setup
    Assist's speech-to-text and text-to-speech pipeline selection is unchanged—only the backend the proxy talks to is stdapi.ai.

  • Pay-Per-Use Pricing
    No per-satellite or per-minute voice assistant fees. Pay only Amazon Transcribe and Amazon Polly rates for actual usage.

%%{init: {'flowchart': {'htmlLabels': true}} }%%
flowchart LR
  ha["<img src='../styles/logo_home_assistant.svg' style='height:64px;width:auto;vertical-align:middle;' /> Home Assistant<br/>Assist"] -->|Wyoming| proxy["<img src='../styles/logo_wyoming.png' style='height:64px;width:64px;vertical-align:middle;' /> wyoming-openai"]
  proxy -->|OpenAI API| stdapi["<img src='../styles/logo.svg' style='height:64px;width:auto;vertical-align:middle;' /> stdapi.ai"]
  stdapi --> transcribe["<img src='../styles/logo_amazon_transcribe.svg' style='height:64px;width:auto;vertical-align:middle;' /> Amazon Transcribe"]
  stdapi --> polly["<img src='../styles/logo_amazon_polly.svg' style='height:64px;width:auto;vertical-align:middle;' /> Amazon Polly"]

About Home Assistant Assist and Wyoming

🔗 Links: Home Assistant Assist | wyoming-openai | Wyoming protocol

Home Assistant's Assist voice pipeline speaks Wyoming, a lightweight protocol for local voice satellites and speech services—not the OpenAI or Anthropic APIs directly. wyoming-openai is an open-source proxy that bridges Wyoming to any OpenAI-compatible speech-to-text and text-to-speech backend, which is what lets Assist reach stdapi.ai.

What the proxy adds on top of the audio routes:

  • Wyoming discovery - Advertises configured speech-to-text models and text-to-speech voices to Assist
  • Streaming synthesis - Speaks a response as it is generated, in overlapping chunks, rather than waiting for the whole sentence
  • Format translation - Reassembles the response as raw PCM frames for Assist's audio pipeline

Connect Your Own Instance

Point any Home Assistant instance's Wyoming bridge at stdapi.ai — the deployment underneath doesn't matter to Assist.

Prerequisites

Before you start
  • stdapi.ai deployed - See deployment guide or run locally with Docker
  • Your stdapi.ai URL - reachable from wherever the proxy runs, e.g. https://api.example.com
  • Your API key - From Terraform output or configuration
  • Home Assistant - With the Assist voice pipeline set up
  • A place to run wyoming-openai - A container alongside Home Assistant, e.g. as a Home Assistant OS add-on or a standalone container

Configuration

wyoming-openai is configured through environment variables, split into a speech-to-text half and a text-to-speech half. Point both at your stdapi.ai deployment.

Environment Variables

# Speech to text
STT_OPENAI_URL=https://YOUR_STDAPI_URL/v1
STT_OPENAI_KEY=YOUR_STDAPI_KEY
STT_MODELS=amazon.transcribe

# Text to speech
TTS_OPENAI_URL=https://YOUR_STDAPI_URL/v1
TTS_OPENAI_KEY=YOUR_STDAPI_KEY
TTS_MODELS=amazon.polly-neural
TTS_VOICES=alloy

# Backend selection
STT_BACKEND=OPENAI
TTS_BACKEND=OPENAI

The proxy calls POST /v1/audio/transcriptions (see Audio Transcriptions API) for speech to text and POST /v1/audio/speech (see Audio Speech API) for text to speech, so STT_MODELS must be a speech-to-text-capable model and TTS_MODELS a text-to-speech-capable model from the correct family.

Pin the backend

Left unset, wyoming-openai probes a few well-known self-hosted backends before falling back to a generic OpenAI-compatible one. Setting STT_BACKEND=OPENAI and TTS_BACKEND=OPENAI skips that probing and connects directly.

A cheaper speech-to-text model

STT_MODELS=amazon.nova-2-sonic-v1:0 transcribes through Amazon Nova Sonic instead of Amazon Transcribe: the lowest-cost transcription available here, punctuated and in the language spoken. It answers json and text only — which is all the proxy asks for — and caps a recording at 10 minutes, well beyond any voice command. Keep amazon.transcribe if the same deployment also serves subtitles, timestamps or speaker labels.

Streaming Speech to Text

Enables: recognizing a spoken command phrase by phrase, instead of after the whole utterance has been recorded.

Environment Variables

STT_STREAMING_MODELS=amazon.transcribe

Only the models listed there are called in streaming mode, which is what makes the proxy ask stdapi.ai for a streamed transcription. The gateway returns each phrase as it is recognized whenever the request names the language to expect; if the proxy sends none, set AWS_TRANSCRIBE_STREAM_LANGUAGES on stdapi.ai to the languages your satellites actually speak and those requests take the same fast path. Streamed transcription stages nothing, so it works on a deployment with no S3 bucket configured.

This is the streaming option to use, not the realtime one

stdapi.ai's Realtime API serves speech-to-speech sessions, and a transcription-only session is requested through an ephemeral client secret rather than on the socket — so a client that expects OpenAI's realtime transcription socket gets no transcript from it. Assist's pipeline is turn-based anyway: speech to text, then a conversation agent, then text to speech.

Streaming Text to Speech

Enables: speaking a response as it is generated, instead of waiting for the whole sentence to synthesize.

Environment Variables

TTS_STREAMING_MODELS=amazon.polly-neural

Naming the same model in both TTS_MODELS and TTS_STREAMING_MODELS puts its voice in the proxy's streaming program, so Assist can use it for both a plain synthesis request and a streamed one. The proxy splits a streamed reply into sentences and synthesizes several /v1/audio/speech calls concurrently, then replays the audio in the original order.

Voice Mapping

TTS_VOICES lists OpenAI-style voice names (alloy, echo, fable, and so on); stdapi.ai maps each one to an Amazon Polly voice of matching gender and language. List one entry per voice you want Assist to offer.

Conversation Agent

Enables: the middle stage of the Assist pipeline — turning the recognized text into an answer and into device actions.

The speech halves above use the Wyoming bridge; the conversation agent does not need one. Home Assistant's built-in Ollama integration talks to stdapi.ai directly through the Ollama-compatible API.

Home Assistant 2026.4 and later splits this across two dialogs: the first adds the server, the second adds a conversation agent on it as a config subentry. Each numbered list below is one dialog, and the second one only opens once the first has been submitted.

Dialog 1 — add the server

Settings → Devices & Services → Add Integration → Ollama

  1. URL: your stdapi.ai deployment URL followed by OLLAMA_ROUTES_PREFIX, e.g. https://YOUR_STDAPI_URL/ollama by default — Home Assistant appends /api/chat itself
  2. API key: your stdapi.ai key. A local Ollama needs no credentials, so this field is easy to skip; without it every request is refused with 401 and the dialog fails with an invalid-authentication error

Submitting the dialog validates the connection with GET /api/tags, so a wrong URL or a missing key is reported here rather than at the first spoken command.

Dialog 2 — add the conversation agent

On the entry the first dialog created, choose Add conversation agent.

  1. Name: what this agent, its device and its conversation.* entity are called; one server can carry several agents, each with its own model and settings
  2. Model: pick one from the list, which is what GET /api/tags publishes. The names a local Ollama would offer, such as llama3.2:latest, are not served here — and a name that is not on the list makes Home Assistant try to download it
  3. Control Home Assistant: select Assist to let the agent operate devices instead of only answering questions. This is what attaches Home Assistant's tool definitions to every request, so pick a model that supports tool calling

Each conversation agent registers its own conversation.* entity, named after the agent. Select that entity as the conversation agent of your Assist pipeline.


Limits and behaviour to know

The proxy speaks the Wyoming protocol over its own TCP port, not HTTP—there is no /health endpoint to check readiness with a plain web request. Wait for a successful Wyoming describe exchange (or check the container logs) rather than polling an HTTP path.

Deploy the Full Stack on AWS

The sample below is one worked example of a credible AWS deployment, not the only architecture that works. The gateway is a normal HTTP service, and Home Assistant, wyoming-openai and stdapi.ai itself can run anywhere you like — your own ECS or EKS cluster, EC2, another cloud, or a laptop.

Architecture

The diagram below is the topology the Terraform sample builds: a public-facing Home Assistant behind an ALB, wyoming-openai as a sidecar in the same ECS task, and the stdapi.ai gateway as a separate, internally-reachable service in the same VPC.

%%{init: {'flowchart': {'htmlLabels': true, 'nodeSpacing': 20, 'rankSpacing': 40, 'subGraphTitleMargin': {'top': 8, 'bottom': 10}}} }%%
flowchart TB
  user["👤 Household members<br/>(browser · Assist microphone)"]

  subgraph public["Your VPC · public subnets"]
    alb["<img src='../styles/logo_amazon_load_balancing.svg' style='height:40px;width:auto;vertical-align:middle;' /> Application Load Balancer<br/>HTTPS · ACM cert, when a custom domain is set"]
  end

  subgraph private["Your VPC · private app subnets — no inbound route from the internet"]
    ha["<img src='../styles/logo_home_assistant.svg' style='height:40px;width:auto;vertical-align:middle;' /> Home Assistant<br/>ECS Fargate task"]
    wyoming["<img src='../styles/logo_wyoming.png' style='height:40px;width:40px;vertical-align:middle;' /> wyoming-openai<br/>sidecar in the same task"]
    efs["<img src='../styles/logo_amazon_efs.svg' style='height:40px;width:auto;vertical-align:middle;' /> Amazon EFS<br/>.storage · configuration.yaml<br/>encrypted · one task only"]
    rds["Amazon RDS for PostgreSQL<br/>recorder database · Multi-AZ<br/>encrypted"]
    stdapi["<img src='../styles/logo.svg' style='height:40px;width:auto;vertical-align:middle;' /> stdapi.ai<br/>ECS Fargate"]
    egress["<img src='../styles/logo_amazon_vpc.svg' style='height:40px;width:auto;vertical-align:middle;' /> NAT gateways<br/>one per Availability Zone"]
  end

  subgraph regional["AWS service endpoints · your account, the regions you configure"]
    transcribe["<img src='../styles/logo_amazon_transcribe.svg' style='height:40px;width:auto;vertical-align:middle;' /> Amazon Transcribe"]
    polly["<img src='../styles/logo_amazon_polly.svg' style='height:40px;width:auto;vertical-align:middle;' /> Amazon Polly"]
    bedrock["<img src='../styles/logo_amazon_bedrock.svg' style='height:40px;width:auto;vertical-align:middle;' /> Amazon Bedrock"]
    s3["<img src='../styles/logo_amazon_s3.svg' style='height:40px;width:auto;vertical-align:middle;' /> Amazon S3<br/>SSE-KMS"]
    cw["<img src='../styles/logo_amazon_cloudwatch.svg' style='height:40px;width:auto;vertical-align:middle;' /> Amazon CloudWatch<br/>container logs"]
    transcribe ~~~ s3
    polly ~~~ cw
    bedrock ~~~ transcribe
  end

  user -->|"HTTPS · TLS 1.2+"| alb
  alb -->|"HTTP · private subnet"| ha
  ha -->|"Wyoming · TCP<br/>localhost, same task"| wyoming
  wyoming -->|"OpenAI API · API key<br/>private DNS, no public endpoint"| stdapi
  ha --> efs
  ha -->|"PostgreSQL · TLS, verify-full"| rds
  ha -->|"HTTPS · SigV4<br/>seeds configuration.yaml, first boot only"| s3
  stdapi --> egress
  egress -->|"HTTPS · SigV4"| transcribe
  egress -->|"HTTPS · SigV4"| polly
  egress -->|"HTTPS · SigV4"| bedrock
  egress -->|"HTTPS · SigV4<br/>stages audio for non-streaming transcription"| s3
  egress --> cw

The ALB is the only public address in the picture, and it forwards only to Home Assistant — stdapi.ai has no listener of its own and is reached exclusively through AWS Cloud Map private DNS from the wyoming-openai sidecar. A household's .storage and configuration.yaml come to rest on the single EFS volume mounted into the Home Assistant task, and the recorder history (state and long-term statistics) in a Multi-AZ Amazon RDS for PostgreSQL instance — neither on the gateway; the gateway itself is stateless and only its egress path crosses the VPC boundary, over HTTPS with SigV4, to Amazon Transcribe, Amazon Polly and Amazon Bedrock.

What Each AWS Service Does Here

AWS service Role in this integration Where it is configured
Amazon ECS on AWS Fargate Runs Home Assistant and wyoming-openai as containers in one task, and the stdapi.ai gateway as a separate service Terraform sample (home_assistant.tf)
Elastic Load Balancing Public entry point for Home Assistant; terminates TLS when a custom domain and certificate are configured Terraform sample (alb.tf)
AWS Cloud Map Private DNS name wyoming-openai uses to reach the gateway, with no public endpoint Terraform sample (service_discovery_dns_name)
Amazon Transcribe Speech-to-text behind POST /v1/audio/transcriptions STT_MODELS (wyoming-openai)
Amazon Polly Text-to-speech behind POST /v1/audio/speech, streamed as concurrent per-sentence calls TTS_MODELS / TTS_STREAMING_MODELS (wyoming-openai)
Amazon Bedrock Conversation-agent answers behind POST /ollama/api/chat, driving Assist's device tool calls home_assistant_conversation_model variable (Terraform sample, home_assistant.tf)
Amazon EFS Home Assistant's .storage and configuration.yaml; a second concurrent writer would corrupt it, so the task is pinned to exactly one Terraform sample (home_assistant.tf, EFS mount point)
Amazon RDS for PostgreSQL Home Assistant's recorder database — history and long-term statistics — as a Multi-AZ instance Terraform sample (postgres.tf)
Amazon S3 Seeds configuration.yaml on first boot through a read-only S3 Files mount, and on the gateway side stages audio for non-streaming transcription Terraform sample (config seed) / gateway module default bucket
AWS KMS Customer-managed keys encrypting the EFS volume, the RDS instance and the S3 buckets ECS module, postgres.tf and gateway module defaults
Amazon CloudWatch Container logs for both ECS services ECS module and gateway module defaults
AWS IAM Separate task roles; the gateway's role carries the module's least-privilege policy for the AWS services it fronts — Transcribe, Polly and Bedrock in this flow IAM permissions

Security Measures in This Flow

  • Authentication — wyoming-openai calls the gateway with a stdapi.ai API key that Terraform generates (api_key_create = true) and injects as STT_OPENAI_KEY/TTS_OPENAI_KEY container secrets; the sample's ALB security group additionally restricts inbound traffic to the deploying operator's own IP address.
  • Encryption in transit — HTTPS from the browser to the ALB when a custom domain and certificate are configured; Wyoming stays inside the ECS task over localhost; HTTPS with SigV4 from the gateway to Amazon Transcribe, Amazon Polly and Amazon Bedrock; PostgreSQL over TLS (sslmode=verify-full) from Home Assistant to the Amazon RDS instance.
  • Encryption at rest — the EFS volume backing Home Assistant's state, the Amazon RDS instance, and both S3 buckets (config seed, gateway staging) use customer-managed KMS keys.
  • Least privilege — the gateway's task role is scoped to the AWS services the module fronts, of which this flow exercises the Transcribe, Polly and Bedrock actions; Home Assistant's task role carries none of them.
  • Content policy — a Bedrock guardrail, if configured on the gateway, checks the text to synthesize as INPUT on /v1/audio/speech and the produced transcript as OUTPUT on /v1/audio/transcriptions, through the ApplyGuardrail API rather than a native chat-style integration.
  • Data handling — the gateway holds request audio in memory, or briefly in its own S3 bucket when staging a non-streaming transcription job, and does not persist it; Home Assistant's own .storage and configuration.yaml stay on the EFS volume in your account, with its recorder history and long-term statistics in the Amazon RDS instance instead.

What's Included

Deploy Home Assistant, wyoming-openai, and stdapi.ai together on ECS Fargate:

📦 stdapi-ai/samples/getting_started_home_assistant

What's included:

  • Home Assistant and wyoming-openai in the same ECS Fargate task, talking over localhost
  • stdapi.ai gateway connected to Amazon Bedrock, Amazon Transcribe, and Amazon Polly
  • configuration.yaml seeded on first boot with the reverse-proxy trust settings Home Assistant needs behind an ALB
  • Both container images pulled directly and anonymously from ghcr.io — no local build, no registry credential
  • HTTPS-capable ALB on your own domain (needed for microphone access in the browser)

Demonstration sample, not a production Home Assistant deployment

AWS Fargate has no route to your home network, so Zigbee/Z-Wave USB dongles, mDNS device discovery, and other LAN-only integrations do not work here. Use it to try Assist voice through Amazon Transcribe/Polly, or as a starting point for a self-hosted, cloud-reachable instance you administer through the web UI. If you already run Home Assistant at home, the sample's README covers deploying only the cloud-side pieces instead of moving Home Assistant itself.

Deploy:

git clone https://github.com/stdapi-ai/samples.git
cd samples/getting_started_home_assistant/terraform
tofu init
tofu apply

Nothing is left manual after tofu apply: a bootstrap container in the same ECS task drives Home Assistant's own APIs to create the owner account, finish onboarding, add the Wyoming integration, add the Ollama conversation agent, and make an Assist pipeline bound to all three the preferred one. Sign in with the credentials Terraform prints and the assistant answers. The conversation model is a Terraform variable, so the two dialogs above are what you follow only when you configure Home Assistant yourself. See the sample's README for the details.


What It Costs to Run

Charge Driver
stdapi.ai licence $0.10 per gateway container-hour, metered through AWS Marketplace, with a 14-day free trial on the licence
ECS Fargate Two services — the Home Assistant + wyoming-openai task, pinned to exactly one, and the gateway, sized independently
Load balancing and networking One ALB, plus the NAT gateways — one per Availability Zone — the private subnets egress through
Amazon EFS Standing storage and throughput for .storage and configuration.yaml
Amazon RDS for PostgreSQL db.t4g.micro, Multi-AZ, storage scaling to the recorder's history and long-term statistics
Amazon Polly Billed per character of text synthesized, not per token
Amazon Transcribe Billed per second of audio transcribed, not per token
Amazon Bedrock Billed per input/output token for the conversation agent's answers (home_assistant_conversation_model)

Read a model's price before you send anything to it with GET /model_pricing. Setting COST_TRACKING=true additionally puts a per-request cost on each usage entry — estimated from published AWS prices, not read back from your invoice.


What to Watch

The gateway logs Polly usage — input_characters, always on the request event — and Transcribe usage — input_seconds, on request normally or on request_stream if you turn on STT_STREAMING_MODELS — with execution_time_ms on every entry. Turning on CLOUDWATCH_METRICS republishes those counts as EMF metrics in the stdapi namespace, dimensioned by Model: Count for characters, Seconds for audio duration.

fields path, execution_time_ms
| filter type = "request" and (path = "/v1/audio/transcriptions" or path = "/v1/audio/speech")
| stats count(*) as calls, avg(execution_time_ms) as avg_ms, pct(execution_time_ms, 95) as p95_ms by path
| sort path

A rising p95 on either path is what a household notices as a slow turn, before it shows up in any cost report.

Next Steps