├── public
├── robots.txt
├── favicon.ico
├── logo192.png
├── logo512.png
├── manifest.json
└── index.html
├── src
├── setupTests.js
├── App.test.js
├── index.css
├── reportWebVitals.js
├── index.js
├── App.css
├── logo.svg
└── App.js
├── .gitignore
├── server
└── server.js
├── package.json
└── README.md
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AssemblyAI-Community/realtime-react-example/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AssemblyAI-Community/realtime-react-example/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AssemblyAI-Community/realtime-react-example/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render();
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | .env
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import App from './App';
5 | import reportWebVitals from './reportWebVitals';
6 |
7 | const root = ReactDOM.createRoot(document.getElementById('root'));
8 | root.render(
9 |
10 |
11 |
12 | );
13 |
14 | // If you want to start measuring performance in your app, pass a function
15 | // to log results (for example: reportWebVitals(console.log))
16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
17 | reportWebVitals();
18 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const cors = require('cors');
3 | const axios = require('axios');
4 | require('dotenv').config();
5 |
6 | const app = express();
7 | app.use(express.json());
8 | app.use(cors());
9 |
10 | app.get('/token', async (req, res) => {
11 | const expiresInSeconds = 500;
12 | const url = `https://streaming.assemblyai.com/v3/token?expires_in_seconds=${expiresInSeconds}`;
13 |
14 | try {
15 | const response = await axios.get(url, {
16 | headers: {
17 | Authorization: process.env.ASSEMBLYAI_API_KEY,
18 | },
19 | });
20 |
21 | res.json({ token: response.data.token });
22 | } catch (error) {
23 | console.error("Error generating temp token:", error.response?.data || error.message);
24 | res.status(500).json({ error: "Failed to fetch token" });
25 | }
26 | });
27 |
28 | app.set('port', 8000);
29 | const server = app.listen(app.get('port'), () => {
30 | console.log(`Server is running on port ${server.address().port}`);
31 | });
32 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | header {
2 | background: #09032f;
3 | color: white;
4 | height: 20vh;
5 | display: flex;
6 | flex-direction: column;
7 | justify-content: center;
8 | padding-left: 2.5%;
9 | box-shadow: 0 0 32px 0 rgb(0 0 0 / 16%);
10 | }
11 |
12 | .header__title {
13 | font-size: 35px;
14 | font-weight: 400;
15 | margin-bottom: .20em;
16 | }
17 |
18 | .header__sub-title {
19 | font-size: 18px;
20 | font-weight: 200;
21 | }
22 |
23 | .real-time-interface {
24 | padding: 1.25% 0 0 2.5%;
25 | }
26 |
27 | .real-time-interface__button {
28 | display: flex;
29 | padding: .5em;
30 | border-radius: .5rem;
31 | justify-content: center;
32 | cursor: pointer;
33 | background: #6b2bd6;
34 | color: white;
35 | width: 10%;
36 | font-size: 18px;
37 | }
38 |
39 | .real-time-interface__title {
40 | margin-bottom: .5em;
41 | font-size: 18px;
42 | font-weight: 525;
43 | }
44 |
45 | .real-time-interface__message {
46 | text-align: left;
47 | width: 95%;
48 | margin-top: .5em;
49 | font-size: 19px;
50 | font-weight: 400;
51 | box-shadow: 0 0 32px 0 rgb(0 0 0 / 16%);
52 | padding: 1em;
53 | transition: .5s ease-in;
54 | }
55 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "real-time-react",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.17.0",
7 | "@testing-library/react": "^13.4.0",
8 | "@testing-library/user-event": "^13.5.0",
9 | "assemblyai": "^4.4.5",
10 | "axios": "^1.11.0",
11 | "concurrently": "^8.2.1",
12 | "cors": "^2.8.5",
13 | "dotenv": "^16.3.1",
14 | "express": "^4.18.2",
15 | "react": "^18.2.0",
16 | "react-dom": "^18.2.0",
17 | "react-scripts": "5.0.1",
18 | "recordrtc": "^5.6.2",
19 | "web-vitals": "^2.1.4"
20 | },
21 | "scripts": {
22 | "start": "concurrently \"react-scripts start\" \"cd server && node server.js\"",
23 | "build": "react-scripts build",
24 | "test": "react-scripts test",
25 | "eject": "react-scripts eject"
26 | },
27 | "eslintConfig": {
28 | "extends": [
29 | "react-app",
30 | "react-app/jest"
31 | ]
32 | },
33 | "browserslist": {
34 | "production": [
35 | ">0.2%",
36 | "not dead",
37 | "not op_mini all"
38 | ],
39 | "development": [
40 | "last 1 chrome version",
41 | "last 1 firefox version",
42 | "last 1 safari version"
43 | ]
44 | },
45 | "devDependencies": {
46 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AssemblyAI Real-Time Microphone Transcription-React Example
2 |
3 | This open-source repo provided by AssemblyAI displays how to use our Real-time API with the React framework.
4 |
5 | In this app, we grab an audio stream from the user's computer and then send that over a WebSocket to AssemblyAI for Real-time transcription. Once AssemblyAI begins transcribing, the app displays the text in the browser. This is accomplished using Express for our backend and React for our frontend.
6 |
7 | ## How To Install and Run the Project
8 |
9 | ##### ❗Important❗
10 |
11 | - Before running this app, you need to upgrade your AssemblyAI account. The real-time API is only available to upgraded accounts at this time.
12 | - Running the app before upgrading will cause an **error with a 402 status code.** ⚠️
13 | - To upgrade your account you need to add a card. You can do that in your dashboard [here](https://www.assemblyai.com/app/)!
14 |
15 | ##### Instructions
16 |
17 | 1. Clone the repo to your local machine.
18 | 2. Open a terminal in the main directory housing the project. In this case `real-time-react`.
19 | 3. Run `npm install` to ensure all dependencies are installed.
20 | 4. Add a `.env` file to the `server` folder. Add your AssemblyAI key to the `.env` file. You can find your API key on the "Account" page in your dashboard [here](https://www.assemblyai.com/app/account). Copy and paste it into the `.env` file replacing "YOUR-PERSONAL-API-KEY" with your own key:
21 |
22 | ```
23 | ASSEMBLYAI_API_KEY="YOUR-PERSONAL-API-KEY"
24 | ```
25 |
26 | 5. Start the app with the command `npm start`. The app will run on port 3000. Open `http://localhost:3000/` in your browser and click "Record" to receive live transcription.
27 |
28 | ## Further Documentation
29 |
30 | - [AssemblyAI Real-Time Documentation](https://www.assemblyai.com/docs/speech-to-text/streaming)
31 | - [recordrtc](https://www.npmjs.com/package/recordrtc)
32 | - [Express](https://expressjs.com/)
33 |
34 | ## Contact Us
35 |
36 | If you have any questions, please feel free to reach out to our Support team - support@assemblyai.com!
37 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import './App.css';
2 | import { useRef, useState } from 'react';
3 |
4 | export default function Home() {
5 | const socket = useRef(null);
6 | const audioContext = useRef(null);
7 | const mediaStream = useRef(null);
8 | const scriptProcessor = useRef(null);
9 |
10 | const [isRecording, setIsRecording] = useState(false);
11 | const [transcripts, setTranscripts] = useState({});
12 |
13 | const getToken = async () => {
14 | const response = await fetch('http://localhost:8000/token');
15 | const data = await response.json();
16 |
17 | if (!data || !data.token) {
18 | alert('Failed to get token');
19 | return null;
20 | }
21 |
22 | return data.token;
23 | };
24 |
25 | const startRecording = async () => {
26 | const token = await getToken();
27 | if (!token) return;
28 |
29 | const wsUrl = `wss://streaming.assemblyai.com/v3/ws?sample_rate=16000&formatted_finals=true&token=${token}`;
30 | socket.current = new WebSocket(wsUrl);
31 |
32 | const turns = {}; // for storing transcript updates per turn
33 |
34 | socket.current.onopen = async () => {
35 | console.log('WebSocket connection established');
36 | console.log('Connected to:', wsUrl);
37 | setIsRecording(true);
38 |
39 | mediaStream.current = await navigator.mediaDevices.getUserMedia({ audio: true });
40 | audioContext.current = new AudioContext({ sampleRate: 16000 });
41 |
42 | const source = audioContext.current.createMediaStreamSource(mediaStream.current);
43 | scriptProcessor.current = audioContext.current.createScriptProcessor(4096, 1, 1);
44 |
45 | source.connect(scriptProcessor.current);
46 | scriptProcessor.current.connect(audioContext.current.destination);
47 |
48 | scriptProcessor.current.onaudioprocess = (event) => {
49 | if (!socket.current || socket.current.readyState !== WebSocket.OPEN) return;
50 |
51 | const input = event.inputBuffer.getChannelData(0);
52 | const buffer = new Int16Array(input.length);
53 | for (let i = 0; i < input.length; i++) {
54 | buffer[i] = Math.max(-1, Math.min(1, input[i])) * 0x7fff;
55 | }
56 | socket.current.send(buffer.buffer);
57 | };
58 | };
59 |
60 | socket.current.onmessage = (event) => {
61 | console.log('Received message:', event.data);
62 | const message = JSON.parse(event.data);
63 |
64 | if (message.type === 'Turn') {
65 | const { turn_order, transcript } = message;
66 | turns[turn_order] = transcript;
67 |
68 | setTranscripts({ ...turns });
69 | }
70 | };
71 |
72 | socket.current.onerror = (err) => {
73 | console.error('WebSocket error:', err);
74 | stopRecording();
75 | };
76 |
77 | socket.current.onclose = () => {
78 | console.log('WebSocket closed');
79 | socket.current = null;
80 | };
81 | };
82 |
83 | const stopRecording = () => {
84 | setIsRecording(false);
85 |
86 | if (scriptProcessor.current) {
87 | scriptProcessor.current.disconnect();
88 | scriptProcessor.current = null;
89 | }
90 |
91 | if (audioContext.current) {
92 | audioContext.current.close();
93 | audioContext.current = null;
94 | }
95 |
96 | if (mediaStream.current) {
97 | mediaStream.current.getTracks().forEach(track => track.stop());
98 | mediaStream.current = null;
99 | }
100 |
101 | if (socket.current) {
102 | socket.current.send(JSON.stringify({ type: 'Terminate' }));
103 | socket.current.close();
104 | socket.current = null;
105 | }
106 | };
107 |
108 | const orderedTranscript = Object.keys(transcripts)
109 | .sort((a, b) => Number(a) - Number(b))
110 | .map((k) => transcripts[k])
111 | .join(' ');
112 |
113 | return (
114 |
115 |
121 |
122 |
Click start to begin recording!
123 | {isRecording ? (
124 |
127 | ) : (
128 |
131 | )}
132 |
133 |
134 |
Transcript: {orderedTranscript}
135 |
136 |
137 | );
138 | }
--------------------------------------------------------------------------------