Skip to content

Commit 88f6727

Browse files
committed
Squash
1 parent dda3f8d commit 88f6727

40 files changed

+8289
-605
lines changed

biome.json

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
"formatter": {
77
"indentStyle": "space",
88
"indentWidth": 2,
9-
"ignore": ["./packages/*/dist/**", "*/**/.next/**", "*/**/node_modules/*"]
9+
"ignore": [
10+
"./packages/*/dist/**",
11+
"*/**/.next/**",
12+
"*/**/node_modules/*",
13+
"./examples/nextjs/public/*"
14+
]
1015
},
1116
"javascript": {
1217
"formatter": {
@@ -18,6 +23,11 @@
1823
"rules": {
1924
"recommended": true
2025
},
21-
"ignore": ["./packages/*/dist/**", "*/**/.next/**", "*/**/node_modules/*"]
26+
"ignore": [
27+
"./packages/*/dist/**",
28+
"*/**/.next/**",
29+
"*/**/node_modules/*",
30+
"./examples/nextjs/public/*"
31+
]
2232
}
2333
}

examples/nextjs/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
public/js/pcm-audio-worklet.min.js
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/next.config.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,28 @@
1+
import path from 'node:path';
2+
import CopyWebpackPlugin from 'copy-webpack-plugin';
13
import type { NextConfig } from 'next';
24

35
const nextConfig: NextConfig = {
4-
/* config options here */
6+
webpack: (config, { isServer }) => {
7+
// Use CopyWebpackPlugin to copy the file to the public directory
8+
if (!isServer) {
9+
config.plugins.push(
10+
new CopyWebpackPlugin({
11+
patterns: [
12+
{
13+
from: path.resolve(
14+
__dirname,
15+
'node_modules/@speechmatics/browser-audio-input/dist/pcm-audio-worklet.min.js',
16+
),
17+
to: path.resolve(__dirname, 'public/js/[name][ext]'),
18+
},
19+
],
20+
}),
21+
);
22+
}
23+
24+
return config;
25+
},
526
};
627

728
export default nextConfig;

examples/nextjs/package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010
"lint": "next lint"
1111
},
1212
"dependencies": {
13-
"@speechmatics/flow-client-react": "workspace:*",
14-
"@speechmatics/browser-audio-input-react": "workspace:*",
15-
"@speechmatics/auth": "workspace:*",
1613
"@picocss/pico": "^2.0.6",
14+
"@speechmatics/auth": "workspace:*",
15+
"@speechmatics/browser-audio-input": "workspace:*",
16+
"@speechmatics/browser-audio-input-react": "workspace:*",
17+
"@speechmatics/flow-client-react": "workspace:*",
18+
"@speechmatics/real-time-client-react": "workspace:*",
1719
"next": "15.0.1",
1820
"react": "19.0.0-rc-69d4b800-20241021",
1921
"react-dom": "19.0.0-rc-69d4b800-20241021",
@@ -23,6 +25,7 @@
2325
"@types/node": "^20",
2426
"@types/react": "^18",
2527
"@types/react-dom": "^18",
28+
"copy-webpack-plugin": "^12.0.2",
2629
"typescript": "^5"
2730
}
2831
}

examples/nextjs/src/app/actions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
import { createSpeechmaticsJWT } from '@speechmatics/auth';
44

5-
export async function getJWT() {
5+
export async function getJWT(type: 'flow' | 'rt') {
66
const apiKey = process.env.API_KEY;
77
if (!apiKey) {
88
throw new Error('Please set the API_KEY environment variable');
99
}
1010

11-
return createSpeechmaticsJWT({ type: 'flow', apiKey, ttl: 60 });
11+
return createSpeechmaticsJWT({ type, apiKey, ttl: 60 });
1212
}

examples/nextjs/src/app/flow/Component.tsx

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
'use client';
22

3-
import { useCallback, useState } from 'react';
3+
import { use, useCallback, useState } from 'react';
44

5-
import {
6-
usePcmMicrophoneAudio,
7-
usePlayPcm16Audio,
8-
} from '../../lib/audio-hooks';
5+
import { usePlayPcm16Audio } from '../../lib/audio-hooks';
96
import { ErrorBoundary } from 'react-error-boundary';
107
import { Controls } from './Controls';
118
import { Status } from './Status';
129
import { ErrorFallback } from '../../lib/components/ErrorFallback';
1310
import { OutputView } from './OutputView';
1411
import { useFlow, useFlowEventListener } from '@speechmatics/flow-client-react';
1512
import { getJWT } from '../actions';
13+
import {
14+
usePcmAudioListener,
15+
usePcmAudioRecorder,
16+
} from '@speechmatics/browser-audio-input-react';
1617

1718
export default function Component({
1819
personas,
@@ -31,13 +32,12 @@ export default function Component({
3132

3233
const [loading, setLoading] = useState(false);
3334

34-
const [mediaStream, setMediaStream] = useState<MediaStream>();
35+
const { startRecording, stopRecording, mediaStream, isRecording } =
36+
usePcmAudioRecorder();
3537

36-
const { startRecording, stopRecording, isRecording } = usePcmMicrophoneAudio(
37-
(audio) => {
38-
sendAudio(audio);
39-
},
40-
);
38+
usePcmAudioListener((audio) => {
39+
sendAudio(audio);
40+
});
4141

4242
const startSession = useCallback(
4343
async ({
@@ -47,7 +47,7 @@ export default function Component({
4747
try {
4848
setLoading(true);
4949

50-
const jwt = await getJWT();
50+
const jwt = await getJWT('flow');
5151

5252
const audioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
5353
setAudioContext(audioContext);
@@ -64,8 +64,7 @@ export default function Component({
6464
},
6565
});
6666

67-
const mediaStream = await startRecording(audioContext, deviceId);
68-
setMediaStream(mediaStream);
67+
await startRecording({ audioContext, deviceId });
6968
} finally {
7069
setLoading(false);
7170
}
Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { fetchPersonas, FlowProvider } from '@speechmatics/flow-client-react';
22
import Component from './Component';
3+
import { PcmAudioRecorderProvider } from '@speechmatics/browser-audio-input-react';
34

45
export default async function Home() {
56
const personas = await fetchPersonas();
67

78
return (
8-
<FlowProvider appId="nextjs-example">
9-
<Component personas={personas} />
10-
</FlowProvider>
9+
<PcmAudioRecorderProvider workletScriptURL="/js/pcm-audio-worklet.min.js">
10+
<FlowProvider appId="nextjs-example">
11+
<Component personas={personas} />
12+
</FlowProvider>
13+
</PcmAudioRecorderProvider>
1114
);
1215
}
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+
}

0 commit comments

Comments
 (0)