Integrate the SDK › Implementation
Wire up the voice session
In this step you build the voice session — the piece of your code that will listen and fill the form at runtime. You create one per form from the client, with the form object you built in the previous step:
- React:
useSpeechToForm() - Angular:
injectSpeechToForm() - JavaScript:
client.speechToForm()
The options are the same everywhere, and so is what you get back: the controls to start and stop listening, one state object to render from, and the recognized values keyed by your field ids.
The examples on this page share one scenario, a patient-intake form: a receptionist presses Speak and lets the patient talk. The whole wiring, using the main options, per framework — the switch compares your form as it is today with the same form once the session is in:
import { useForm } from "react-hook-form";import { useSpeechToForm } from "@speechineer/react";import { intakeForm } from "./intake-form"; const savedDraft = { insurance: "Public" }; // the draft your form holds — not a submitted record export function IntakeForm() { const { register, handleSubmit, setValue, getValues } = useForm<Intake>({ defaultValues: savedDraft }); const voice = useSpeechToForm({ form: intakeForm, spokenLanguage: "en", // the language the patient will speak transcript: true, // also stream the spoken text initialValues: getValues(), // what the form holds now — resent to Speechineer on every start onFieldValue: (id, value) => value != null && setValue(id as keyof Intake, value as never, { shouldDirty: true }), onError: (error) => console.warn(error.code, error.message), }); return ( <form onSubmit={handleSubmit(save)}> <label>Full name <input {...register("full_name")} /></label> <label>Date of birth <input type="date" {...register("date_of_birth")} /></label> <label>Insurance <select {...register("insurance")}> <option>None</option><option>Public</option><option>Private</option> </select> </label> <label>Reason for visit <textarea {...register("reason")} /></label> <button type="submit">Save</button> {/* isListening & co. live directly on voice — the hook re-renders on every state change, no getState() needed */} <button type="button" onClick={voice.isListening ? voice.stop : () => void voice.start()} disabled={voice.isConnecting}> {voice.isConnecting ? "Connecting…" : voice.isListening ? "Stop" : "Speak"} </button> {voice.error && <p role="alert">{voice.error.message}</p>} <label>Transcript {/* the live transcription, streaming while the user speaks */} <textarea rows={4} readOnly aria-live="polite" value={voice.transcript} /> </label> </form> );}added · changed — everything else is untouched.
import { Component } from "@angular/core";import { FormControl, FormGroup, ReactiveFormsModule } from "@angular/forms";import { injectSpeechToForm } from "@speechineer/angular";import { intakeForm } from "./intake-form"; @Component({ selector: "intake-form", imports: [ReactiveFormsModule], template: ` <form [formGroup]="form" (ngSubmit)="save()"> <label>Full name <input formControlName="full_name" /></label> <label>Date of birth <input type="date" formControlName="date_of_birth" /></label> <label>Insurance <select formControlName="insurance"> <option>None</option><option>Public</option><option>Private</option> </select> </label> <label>Reason for visit <textarea formControlName="reason"></textarea></label> <button type="submit">Save</button> <!-- isListening & co. are signals on voice — always current, no getState() needed --> <button type="button" (click)="voice.isListening() ? voice.stop() : voice.start()" [disabled]="voice.isConnecting()"> {{ voice.isConnecting() ? "Connecting…" : voice.isListening() ? "Stop" : "Speak" }} </button> @if (voice.error(); as error) { <p role="alert">{{ error.message }}</p> } <label>Transcript <!-- the live transcription, streaming while the user speaks --> <textarea rows="4" readonly aria-live="polite" [value]="voice.transcript()"></textarea> </label> </form> `,})export class IntakeForm { readonly form = new FormGroup({ full_name: new FormControl(""), date_of_birth: new FormControl(""), insurance: new FormControl("None"), reason: new FormControl(""), }); readonly voice = injectSpeechToForm({ form: intakeForm, spokenLanguage: "en", // the language the patient will speak transcript: true, // also stream the spoken text initialValues: { insurance: "Public" }, // keep this fed from your form state — resent on every start onFieldValue: (id, value) => value != null && this.form.get(id)?.setValue(value as never), onError: (error) => console.warn(error.code, error.message), }); save() { /* … */ }}added · changed — everything else is untouched.
<form id="intake"> <label>Full name <input name="full_name" /></label> <label>Date of birth <input name="date_of_birth" type="date" /></label> <label>Insurance <select name="insurance"><option>None</option><option>Public</option><option>Private</option></select> </label> <label>Reason for visit <textarea name="reason"></textarea></label> <button type="submit">Save</button> <button type="button" id="speak">Speak</button> <p id="error" role="alert" hidden></p> <label>Transcript <!-- the live transcription, streaming while the user speaks --> <textarea id="transcript" rows="4" readonly aria-live="polite"></textarea> </label></form> <script type="module"> import { speechineer } from "./speechineer-client.js"; // the client you created once import { intakeForm } from "./intake-form.js"; const formEl = document.querySelector("#intake"); const speak = document.querySelector("#speak"); const errorEl = document.querySelector("#error"); const transcriptEl = document.querySelector("#transcript"); const session = speechineer.speechToForm({ form: intakeForm, spokenLanguage: "en", // the language the patient will speak transcript: true, // also stream the spoken text initialValues: { insurance: "Public" }, // keep this fed from your form state — resent on every start onFieldValue: (id, value) => { if (value == null) return; // not extracted yet — never blank what the user typed const el = formEl.elements.namedItem(id); if (el) el.value = String(value); }, onError: (error) => console.warn(error.code, error.message), }); speak.onclick = () => (session.getState().isListening ? session.stop() : session.start()); session.subscribe((state) => { speak.disabled = state.isConnecting; speak.textContent = state.isConnecting ? "Connecting…" : state.isListening ? "Stop" : "Speak"; errorEl.hidden = !state.error; errorEl.textContent = state.error?.message ?? ""; transcriptEl.value = state.transcript; // the live transcription box }); // Leaving the page: release the microphone and the connections. window.addEventListener("pagehide", () => session.dispose());</script>added · changed — everything else is untouched.
Options
Everything a session takes is one options object:
The ones you will reach for first — each option has its own section below:
form— which form to fill.onFieldValue— receive each recognized value.spokenLanguage— the language the user will speak.transcript— also stream the spoken text.initialValues— continue from values you already have.onError— react to what goes wrong.
form
The form object from the previous step — one configured in your workspace or one defined in code. It is read when a session starts.
Changed while a session runs: takes effect only for the next session, after end().
Additional information in
account
The end user this session is for, when it differs from the client's default account — a shared kiosk where the person changes between sessions, say. Ignored when the client authenticates with a token: the token carries the account. The account object:
Changed while a session runs: takes effect only for the next session, after end().
spokenLanguage
The language the user will speak — independent of the form's language, which is the language of the definition. Detected automatically when omitted; set it when you know it, for faster and more reliable recognition. The two settings being separate is what makes mixed situations work: a German clinic keeps its form defined in German (language: "de") — the record belongs to the clinic, whose staff read the labels and the answers in German — and when a patient happens to speak English, spokenLanguage: "en" is all that changes; the values still land in the same German form, with no second form to maintain.
Changed while a session runs: takes effect at the next start().
transcript
true streams the spoken text alongside the values — transcript in the session state fills while the user speaks, and onTranscript fires with it. Set it when the user should see what was heard.
Changed while a session runs: takes effect only for the next session, after end().
initialValues
Values your form already holds, keyed by field id — a draft, not a submitted record. A common practice is to keep them in sync with the state of your form: recognition then continues from what the user already sees, including anything typed while the session was paused.
Changed while a session runs: takes effect at the next start().
onFieldValue
Called once per recognized value, with (fieldId, value). Values reach you twice — as accumulating session state (values, keyed by field id) and per field through this callback. A refined value replaces the earlier one for the same field, so always overwrite.
Speechineer's backend validates every value against its field type before sending it, so there is nothing to re-check — but the SDK types them unknown, and a field nothing has been extracted for yet arrives as null. A common pattern is to narrow once at the boundary:
// Speechineer's backend has already validated each value against its field type — no re-checking needed:
onFieldValue: (id, value) => {
if (value == null) return; // nothing extracted for this field yet
setValue(id as keyof Intake, value as never, { shouldDirty: true });
},// Speechineer's backend has already validated each value against its field type — no re-checking needed:
onFieldValue: (id, value) => {
if (value == null) return; // nothing extracted for this field yet
this.form.get(id)?.setValue(value as never);
},// Speechineer's backend has already validated each value against its field type — no re-checking needed:
onFieldValue: (id, value) => {
if (value == null) return; // nothing extracted for this field yet
const el = formEl.elements.namedItem(id);
if (el instanceof HTMLInputElement && el.type === "checkbox") el.checked = value === true; // checkbox → boolean
else if (el) el.value = String(value); // everything else renders as text
},Changed while a session runs: takes effect immediately — the latest function is the one called.
Tip
onTranscript
Called with the transcript so far — the full accumulated text, not just the newest words, so you can render it directly. Only fires with transcript set to true.
Changed while a session runs: takes effect immediately — the latest function is the one called.
onStateChange
Called with the whole new state, every time anything in it changes — the same state described in getState. The framework bindings already re-render from it, so reach for this callback only for logging or for pushing the state somewhere else.
Changed while a session runs: takes effect immediately — the latest function is the one called.
onEvent
Called for every status event Speechineer emits for this session — progress, warnings and failures alike, each with a type, a level and a payload. Use it for logging or a live status display; nothing you must react to arrives only here.
Changed while a session runs: takes effect immediately — the latest function is the one called.
Additional information in
onError
Called when something fails in a way you should handle — a rejected request, a denied microphone permission, a lost connection, a quota that is reached. Read error.code to branch and error.recoverable to decide whether to offer a retry; the same error also sits in the session state as error.
Changed while a session runs: takes effect immediately — the latest function is the one called.
Additional information in
onSessionStart
Called once, as soon as the session exists and work can begin, with the session id — keep it to correlate the session with your own logs.
Changed while a session runs: takes effect immediately — the latest function is the one called, though it fires again only when the next session starts.
Results
What the call hands back — the session, its controls and its observable state:
start
Asks for the microphone the first time, connects, and listens. Safe to call again after stop() — it continues the same session.
stop
Pauses listening. Values still being recognized keep arriving, so expect late onFieldValue calls after it and keep applying them.
end
Finishes the session and releases everything. start() afterwards begins a new session.
dispose
Releases the microphone and the connections without finishing the session — for leaving the page. The React and Angular bindings call it for you when the component goes away; in JavaScript, call it yourself.
getState
The session reports one observable state, identical everywhere — React hands it to you as plain values, Angular as signals, JavaScript reads it with getState() or through subscribe. Render from it; never keep a copy.
| Field | What it tells you |
|---|---|
| values | The latest recognized value of every field, keyed by field id. Starts from initialValues; a refined value replaces the earlier one. |
| transcript | The spoken text so far — filled when the session streams it (transcript: true). |
| isListening | The microphone is on and audio is being sent. Drives your talk button. |
| isConnecting | The session or one of its connections is being established. Show a busy state. |
| isEnding | end() is finishing the session, or the microphone is winding down after stop(). |
| error | The last SpeechineerError, or null; cleared when the next start() succeeds. |
| sessionId | The id of the running session, or null — join your own logs on it. |
| status | The lifecycle: idle · starting · active · recovering · ending · failed. |
| connections | Per-connection status (session, audio, results, transcript): closed · connecting · open · closing. |
subscribe
Be told about every state change; returns the unsubscribe function. The framework bindings subscribe for you — in plain JavaScript it is how your UI follows the session.
setOptions
Swaps the options the session reads from — how you change form, language or callbacks between sessions. In React and Angular you never call it: changing what you pass to the hook does the same.
Example
The same wiring once more, this time using every option the session takes — including the per-session account and all the callbacks:
import { useForm } from "react-hook-form";import { useSpeechToForm } from "@speechineer/react";import { intakeForm } from "./intake-form"; const savedDraft = { insurance: "Public" }; // the draft your form holds — not a submitted record export function IntakeForm() { const { register, handleSubmit, setValue, getValues } = useForm<Intake>({ defaultValues: savedDraft }); const voice = useSpeechToForm({ form: intakeForm, spokenLanguage: "en", // the language the patient will speak transcript: true, // also stream the spoken text initialValues: getValues(), // what the form holds now — resent to Speechineer on every start account: { key: patientId }, // this session's end user, when it differs from the client's default onTranscript: (text) => console.log("transcript", text), onSessionStart: (sessionId) => console.log("session", sessionId), onStateChange: (state) => console.log("state", state.status), onEvent: (event) => console.log(event.type, event.level), onFieldValue: (id, value) => value != null && setValue(id as keyof Intake, value as never, { shouldDirty: true }), onError: (error) => console.warn(error.code, error.message), }); return ( <form onSubmit={handleSubmit(save)}> <label>Full name <input {...register("full_name")} /></label> <label>Date of birth <input type="date" {...register("date_of_birth")} /></label> <label>Insurance <select {...register("insurance")}> <option>None</option><option>Public</option><option>Private</option> </select> </label> <label>Reason for visit <textarea {...register("reason")} /></label> <button type="submit">Save</button> {/* isListening & co. live directly on voice — the hook re-renders on every state change, no getState() needed */} <button type="button" onClick={voice.isListening ? voice.stop : () => void voice.start()} disabled={voice.isConnecting}> {voice.isConnecting ? "Connecting…" : voice.isListening ? "Stop" : "Speak"} </button> {voice.error && <p role="alert">{voice.error.message}</p>} <label>Transcript {/* the live transcription, streaming while the user speaks */} <textarea rows={4} readOnly aria-live="polite" value={voice.transcript} /> </label> </form> );}added · changed — everything else is untouched.
import { Component } from "@angular/core";import { FormControl, FormGroup, ReactiveFormsModule } from "@angular/forms";import { injectSpeechToForm } from "@speechineer/angular";import { intakeForm } from "./intake-form"; @Component({ selector: "intake-form", imports: [ReactiveFormsModule], template: ` <form [formGroup]="form" (ngSubmit)="save()"> <label>Full name <input formControlName="full_name" /></label> <label>Date of birth <input type="date" formControlName="date_of_birth" /></label> <label>Insurance <select formControlName="insurance"> <option>None</option><option>Public</option><option>Private</option> </select> </label> <label>Reason for visit <textarea formControlName="reason"></textarea></label> <button type="submit">Save</button> <!-- isListening & co. are signals on voice — always current, no getState() needed --> <button type="button" (click)="voice.isListening() ? voice.stop() : voice.start()" [disabled]="voice.isConnecting()"> {{ voice.isConnecting() ? "Connecting…" : voice.isListening() ? "Stop" : "Speak" }} </button> @if (voice.error(); as error) { <p role="alert">{{ error.message }}</p> } <label>Transcript <!-- the live transcription, streaming while the user speaks --> <textarea rows="4" readonly aria-live="polite" [value]="voice.transcript()"></textarea> </label> </form> `,})export class IntakeForm { readonly form = new FormGroup({ full_name: new FormControl(""), date_of_birth: new FormControl(""), insurance: new FormControl("None"), reason: new FormControl(""), }); readonly voice = injectSpeechToForm({ form: intakeForm, spokenLanguage: "en", // the language the patient will speak transcript: true, // also stream the spoken text initialValues: { insurance: "Public" }, // keep this fed from your form state — resent on every start account: { key: patientId }, // this session's end user, when it differs from the client's default onTranscript: (text) => console.log("transcript", text), onSessionStart: (sessionId) => console.log("session", sessionId), onStateChange: (state) => console.log("state", state.status), onEvent: (event) => console.log(event.type, event.level), onFieldValue: (id, value) => value != null && this.form.get(id)?.setValue(value as never), onError: (error) => console.warn(error.code, error.message), }); save() { /* … */ }}added · changed — everything else is untouched.
<form id="intake"> <label>Full name <input name="full_name" /></label> <label>Date of birth <input name="date_of_birth" type="date" /></label> <label>Insurance <select name="insurance"><option>None</option><option>Public</option><option>Private</option></select> </label> <label>Reason for visit <textarea name="reason"></textarea></label> <button type="submit">Save</button> <button type="button" id="speak">Speak</button> <p id="error" role="alert" hidden></p> <label>Transcript <!-- the live transcription, streaming while the user speaks --> <textarea id="transcript" rows="4" readonly aria-live="polite"></textarea> </label></form> <script type="module"> import { speechineer } from "./speechineer-client.js"; // the client you created once import { intakeForm } from "./intake-form.js"; const formEl = document.querySelector("#intake"); const speak = document.querySelector("#speak"); const errorEl = document.querySelector("#error"); const transcriptEl = document.querySelector("#transcript"); const session = speechineer.speechToForm({ form: intakeForm, spokenLanguage: "en", // the language the patient will speak transcript: true, // also stream the spoken text initialValues: { insurance: "Public" }, // keep this fed from your form state — resent on every start account: { key: patientId }, // this session's end user, when it differs from the client's default onTranscript: (text) => (transcriptEl.value = text), // fill the live transcription box onSessionStart: (sessionId) => console.log("session", sessionId), onStateChange: (state) => console.log("state", state.status), onEvent: (event) => console.log(event.type, event.level), onFieldValue: (id, value) => { if (value == null) return; // not extracted yet — never blank what the user typed const el = formEl.elements.namedItem(id); if (el) el.value = String(value); }, onError: (error) => console.warn(error.code, error.message), }); speak.onclick = () => (session.getState().isListening ? session.stop() : session.start()); session.subscribe((state) => { speak.disabled = state.isConnecting; speak.textContent = state.isConnecting ? "Connecting…" : state.isListening ? "Stop" : "Speak"; errorEl.hidden = !state.error; errorEl.textContent = state.error?.message ?? ""; }); // Leaving the page: release the microphone and the connections. window.addEventListener("pagehide", () => session.dispose());</script>added · changed — everything else is untouched.