Skip to content

Commit d4095e7

Browse files
committed
Squash
1 parent 0b3ce36 commit d4095e7

File tree

18 files changed

+628
-3
lines changed

18 files changed

+628
-3
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"version": "0.2.0",
3+
"configurations": [
4+
{
5+
"command": "npm run dev",
6+
"name": "Run npm run dev",
7+
"request": "launch",
8+
"type": "node-terminal",
9+
"cwd": "${workspaceFolder}"
10+
},
11+
{
12+
"command": "npm run test",
13+
"name": "Run npm run test",
14+
"request": "launch",
15+
"type": "node-terminal",
16+
"cwd": "${workspaceFolder}"
17+
},
18+
{
19+
"name": "Attach by Process ID",
20+
"processId": "${command:PickProcess}",
21+
"request": "attach",
22+
"skipFiles": ["<node_internals>/**"],
23+
"type": "node"
24+
}
25+
]
26+
}

examples/nextjs/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"@speechmatics/browser-audio-input": "workspace:*",
1616
"@speechmatics/browser-audio-input-react": "workspace:*",
1717
"@speechmatics/flow-client-react": "workspace:*",
18+
"@speechmatics/real-time-client-react": "workspace:*",
1819
"next": "15.0.1",
1920
"react": "19.0.0-rc-69d4b800-20241021",
2021
"react-dom": "19.0.0-rc-69d4b800-20241021",
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use client';
2+
3+
import { useRealtimeTranscription } from '@speechmatics/real-time-client-react';
4+
import { Controls } from './Controls';
5+
import { useEffect } from 'react';
6+
import {
7+
usePcmAudioListener,
8+
usePcmAudioRecorder,
9+
} from '@speechmatics/browser-audio-input-react';
10+
import { Output } from './Output';
11+
import { Status } from './Status';
12+
13+
export default function Component() {
14+
const { stopTranscription, sendAudio } = useRealtimeTranscription();
15+
const { stopRecording } = usePcmAudioRecorder();
16+
17+
usePcmAudioListener(sendAudio);
18+
19+
// Cleanup
20+
useEffect(() => {
21+
return () => {
22+
stopTranscription();
23+
stopRecording();
24+
};
25+
}, [stopTranscription, stopRecording]);
26+
27+
return (
28+
<section>
29+
<h3>Real-time Example</h3>
30+
<section className="grid">
31+
<Controls />
32+
<Status />
33+
</section>
34+
<section>
35+
<Output />
36+
</section>
37+
</section>
38+
);
39+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { type ChangeEvent, type FormEvent, useCallback, useState } from 'react';
2+
import {
3+
useAudioDevices,
4+
usePcmAudioRecorder,
5+
} from '@speechmatics/browser-audio-input-react';
6+
import {
7+
type RealtimeTranscriptionConfig,
8+
useRealtimeTranscription,
9+
} from '@speechmatics/real-time-client-react';
10+
import { getJWT } from '../actions';
11+
import { configFromFormData } from '@/lib/real-time/config-from-form-data';
12+
import { RECORDING_SAMPLE_RATE } from '@/lib/constants';
13+
14+
export function Controls() {
15+
const { startTranscription } = useRealtimeTranscription();
16+
const { startRecording } = usePcmAudioRecorder();
17+
18+
const [deviceId, setDeviceId] = useState<string>();
19+
20+
const startSession = useCallback(
21+
async (config: RealtimeTranscriptionConfig) => {
22+
const jwt = await getJWT('rt');
23+
await startTranscription(jwt, config);
24+
await startRecording({ deviceId, sampleRate: RECORDING_SAMPLE_RATE });
25+
},
26+
[startTranscription, startRecording, deviceId],
27+
);
28+
29+
const handleSubmit = useCallback(
30+
(e: FormEvent<HTMLFormElement>) => {
31+
e.preventDefault();
32+
const formData = new FormData(e.currentTarget);
33+
const config = configFromFormData(formData);
34+
config.audio_format = {
35+
type: 'raw',
36+
encoding: 'pcm_f32le',
37+
sample_rate: RECORDING_SAMPLE_RATE,
38+
};
39+
startSession(config);
40+
},
41+
[startSession],
42+
);
43+
44+
return (
45+
<article>
46+
<form onSubmit={handleSubmit}>
47+
<div className="grid">
48+
<MicrophoneSelect setDeviceId={setDeviceId} />
49+
<label>
50+
Select language
51+
<select name="language">
52+
<option value="en" label="English" defaultChecked />
53+
<option value="es" label="Spanish" />
54+
<option value="ar" label="Arabic" />
55+
</select>
56+
</label>
57+
</div>
58+
<div className="grid">
59+
<StartStopButton />
60+
</div>
61+
</form>
62+
</article>
63+
);
64+
}
65+
66+
function StartStopButton() {
67+
const { stopRecording } = usePcmAudioRecorder();
68+
const { stopTranscription } = useRealtimeTranscription();
69+
70+
const stopSession = useCallback(() => {
71+
stopTranscription();
72+
stopRecording();
73+
}, [stopRecording, stopTranscription]);
74+
75+
const connected = useRealtimeTranscription().socketState === 'open';
76+
77+
if (connected) {
78+
return (
79+
<button type="button" onClick={stopSession}>
80+
Stop transcription
81+
</button>
82+
);
83+
}
84+
85+
return <button type="submit">Transcribe audio</button>;
86+
}
87+
88+
function MicrophoneSelect({
89+
setDeviceId,
90+
}: { setDeviceId: (deviceId: string) => void }) {
91+
const devices = useAudioDevices();
92+
93+
switch (devices.permissionState) {
94+
case 'prompt':
95+
return (
96+
<label>
97+
Enable mic permissions
98+
<select
99+
onClick={devices.promptPermissions}
100+
onKeyDown={devices.promptPermissions}
101+
/>
102+
</label>
103+
);
104+
case 'prompting':
105+
return (
106+
<label>
107+
Enable mic permissions
108+
<select aria-busy="true" />
109+
</label>
110+
);
111+
case 'granted': {
112+
const onChange = (e: ChangeEvent<HTMLSelectElement>) => {
113+
setDeviceId(e.target.value);
114+
};
115+
return (
116+
<label>
117+
Select audio device
118+
<select onChange={onChange}>
119+
{devices.deviceList.map((d) => (
120+
<option key={d.deviceId} value={d.deviceId}>
121+
{d.label}
122+
</option>
123+
))}
124+
</select>
125+
</label>
126+
);
127+
}
128+
case 'denied':
129+
return (
130+
<label>
131+
Microphone permission disabled
132+
<select disabled />
133+
</label>
134+
);
135+
default:
136+
devices satisfies never;
137+
return null;
138+
}
139+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { useReducer } from 'react';
2+
import {
3+
type RealtimeServerMessage,
4+
useRealtimeEventListener,
5+
} from '@speechmatics/real-time-client-react';
6+
7+
export function Output() {
8+
const [transcription, dispatch] = useReducer(transcriptReducer, '');
9+
10+
useRealtimeEventListener('receiveMessage', (e) => dispatch(e.data));
11+
12+
return (
13+
<article>
14+
<header>Output</header>
15+
<p>{transcription}</p>
16+
</article>
17+
);
18+
}
19+
20+
function transcriptReducer(acc: string, event: RealtimeServerMessage) {
21+
if (event.message === 'AddTranscript') {
22+
return `${acc} ${event.results.map((result) => result.alternatives?.[0].content).join(' ')}`;
23+
}
24+
25+
return acc;
26+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { usePcmAudioRecorder } from '@speechmatics/browser-audio-input-react';
2+
import { useRealtimeTranscription } from '@speechmatics/real-time-client-react';
3+
4+
export function Status() {
5+
const { socketState, sessionId } = useRealtimeTranscription();
6+
const { isRecording } = usePcmAudioRecorder();
7+
8+
return (
9+
<article>
10+
<header>Status</header>
11+
<dl>
12+
<dt>🔌 Socket is</dt>
13+
<dd>{socketState ?? '(uninitialized)'}</dd>
14+
<dt>💬 Session ID</dt>
15+
<dd>{sessionId ?? '(none)'}</dd>
16+
<dt>🎤 Microphone is</dt>
17+
<dd>{isRecording ? 'recording' : 'not recording'}</dd>
18+
</dl>
19+
</article>
20+
);
21+
}
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
import { RealtimeTranscriptionProvider } from '@speechmatics/real-time-client-react';
2+
import Component from './Component';
3+
import { PcmAudioRecorderProvider } from '@speechmatics/browser-audio-input-react';
4+
15
export default function Page() {
2-
return <h1>Coming soon!</h1>;
6+
return (
7+
<PcmAudioRecorderProvider workletScriptURL="/js/pcm-audio-worklet.min.js">
8+
<RealtimeTranscriptionProvider appId="nextjs-rt-example">
9+
<Component />
10+
</RealtimeTranscriptionProvider>
11+
</PcmAudioRecorderProvider>
12+
);
313
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { RealtimeTranscriptionConfig } from '@speechmatics/real-time-client-react';
2+
3+
// TODO could have zod schemas here
4+
export function configFromFormData(
5+
formData: FormData,
6+
): RealtimeTranscriptionConfig {
7+
const language = formData.get('language')?.toString();
8+
9+
if (!language) {
10+
throw new Error('Language is required');
11+
}
12+
13+
return {
14+
transcription_config: {
15+
language,
16+
max_delay: 1,
17+
},
18+
};
19+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Speechmatics Flow client (React) 🤖 ⚛
2+
3+
React hooks for interacting with the [Speechmatics Flow API](https://docs.speechmatics.com/flow/getting-started).
4+
5+
This package wraps the `@speechmatics/flow-client` package for use in React projects.
6+
7+
## Installlation
8+
9+
```
10+
npm i @speechmatics/flow-client-react
11+
```
12+
13+
> [!WARNING]
14+
> For React Native, make sure to install the [`event-target-polyfill`](https://www.npmjs.com/package/event-target-polyfill) package, or any other polyfill for the [`EventTarget` class](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget)
15+
16+
## Usage
17+
18+
19+
1. Use the `FlowProvider` at a high level of your component tree:
20+
21+
```JSX
22+
import { FlowProvider } from "@speechmatics/flow-client-react";
23+
24+
function RootLayout({children}) {
25+
<FlowProvider appId="your-app-id">
26+
{children}
27+
</FlowProvider>
28+
}
29+
```
30+
Note that`FlowProvider` is a [client component](https://nextjs.org/docs/app/building-your-application/rendering/client-components), like any other context provider. In NextJS, it's best to put this either in a root layout, or inside another client component. For frameworks like Remix which don't use React Server Components, it should work anywhere.
31+
32+
_Note for React Native_: Pass `websocketBinaryType="arraybuffer"` to the `FlowProvider` as it is more reliable than the Blob implementation.
33+
34+
1. Inside a component below the `FlowProvider`:
35+
```JSX
36+
function MyComponent() {
37+
const { startConversation, endConversation, sendAudio } = useFlow()
38+
}
39+
```
40+
41+
42+
1. Connect and start conversation
43+
44+
`startConversation` is a function which requires a JWT to open a websocket and begin a session.
45+
46+
See our documentation about generating JWTs (temporary keys): https://docs.speechmatics.com/introduction/authentication#temporary-keys
47+
48+
An example credentials fetching flow can be found in the [NextJS example](/examples/nextjs/src/lib/fetch-credentials.ts).
49+
50+
```typescript
51+
await startConversation(jwt, {
52+
config: {
53+
template_id: personaId,
54+
template_variables: {},
55+
},
56+
// `audioFormat` is optional. The value below is the default:
57+
audioFormat: {
58+
type: 'raw',
59+
encoding: 'pcm_s16le', // this can also be set to 'pcm_f32le' for 32-bit Float
60+
sample_rate: 16000,
61+
},
62+
});
63+
```
64+
65+
66+
1. Sending audio
67+
68+
The `sendAudio` function above accepts any `ArrayBufferLike`. You should send a buffer with the audio encoded as you requested when calling `startConversation` (either 32-bit float or 16-bit signed integer PCM).
69+
70+
71+
1. Listen for audio and messages
72+
73+
Incoming data from the Flow service can be subscribed to using the `useFlowEventListener` hook:
74+
75+
```TSX
76+
// Handling Messages
77+
useFlowEventListener("message", ({ data }) => {
78+
if (data.message === "AddTranscript") {
79+
// handle transcript message
80+
}
81+
});
82+
83+
// Handling audio
84+
useFlowEventListener("agentAudio", (audio) => {
85+
// Incoming audio data is always 16-bit signed int PCM.
86+
// How you handle this depends on your environment.
87+
myPlayAudioFunction(audio.data);
88+
})
89+
```

0 commit comments

Comments
 (0)