├── 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 |
116 |

Real-Time Transcription (v3)

117 |

118 | Powered by AssemblyAI's latest real-time model 119 |

120 |
121 |
122 |

Click start to begin recording!

123 | {isRecording ? ( 124 | 127 | ) : ( 128 | 131 | )} 132 |
133 |
134 |

Transcript: {orderedTranscript}

135 |
136 |
137 | ); 138 | } --------------------------------------------------------------------------------