├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── server ├── .env.sample └── server.js └── src ├── App.css ├── App.js ├── App.test.js ├── index.css ├── index.js ├── logo.svg ├── reportWebVitals.js └── setupTests.js /.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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | "concurrently": "^8.2.1", 11 | "cors": "^2.8.5", 12 | "dotenv": "^16.3.1", 13 | "express": "^4.18.2", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-scripts": "5.0.1", 17 | "recordrtc": "^5.6.2", 18 | "web-vitals": "^2.1.4" 19 | }, 20 | "scripts": { 21 | "start": "concurrently \"react-scripts start\" \"cd server && node server.js\"", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test", 24 | "eject": "react-scripts eject" 25 | }, 26 | "eslintConfig": { 27 | "extends": [ 28 | "react-app", 29 | "react-app/jest" 30 | ] 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.2%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | }, 44 | "devDependencies": { 45 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AssemblyAI-Community/realtime-react-example/6d91d806d4bfc3e7a5bd7681d77e7aea2ad3a139/public/favicon.ico -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AssemblyAI-Community/realtime-react-example/6d91d806d4bfc3e7a5bd7681d77e7aea2ad3a139/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AssemblyAI-Community/realtime-react-example/6d91d806d4bfc3e7a5bd7681d77e7aea2ad3a139/public/logo512.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /server/.env.sample: -------------------------------------------------------------------------------- 1 | ASSEMBLYAI_API_KEY=[YOUR_API_KEY] -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cors = require('cors'); 3 | const { AssemblyAI } = require('assemblyai'); 4 | require('dotenv').config(); 5 | 6 | const aaiClient = new AssemblyAI({ apiKey: process.env.ASSEMBLYAI_API_KEY }); 7 | const app = express(); 8 | app.use(express.json()); 9 | app.use(cors()); 10 | 11 | app.get('/token', async (req, res) => { 12 | try { 13 | const token = await aaiClient.realtime.createTemporaryToken({ expires_in: 3600 }); 14 | res.json({ token }); 15 | } catch (error) { 16 | res.status(500).json({ error: error.message }); 17 | } 18 | }); 19 | 20 | app.set('port', 8000); 21 | const server = app.listen(app.get('port'), () => { 22 | console.log(`Server is running on port ${server.address().port}`); 23 | }); 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import './App.css'; 2 | import { useRef, useState } from 'react'; 3 | import { RealtimeTranscriber } from 'assemblyai/streaming'; 4 | import * as RecordRTC from 'recordrtc'; 5 | 6 | function App() { 7 | /** @type {React.MutableRefObject} */ 8 | const realtimeTranscriber = useRef(null) 9 | /** @type {React.MutableRefObject} */ 10 | const recorder = useRef(null) 11 | const [isRecording, setIsRecording] = useState(false) 12 | const [transcript, setTranscript] = useState('') 13 | 14 | const getToken = async () => { 15 | const response = await fetch('http://localhost:8000/token'); 16 | const data = await response.json(); 17 | 18 | if (data.error) { 19 | alert(data.error) 20 | } 21 | 22 | return data.token; 23 | }; 24 | 25 | const startTranscription = async () => { 26 | realtimeTranscriber.current = new RealtimeTranscriber({ 27 | token: await getToken(), 28 | sampleRate: 16_000, 29 | }); 30 | 31 | const texts = {}; 32 | realtimeTranscriber.current.on('transcript', transcript => { 33 | let msg = ''; 34 | texts[transcript.audio_start] = transcript.text; 35 | const keys = Object.keys(texts); 36 | keys.sort((a, b) => a - b); 37 | for (const key of keys) { 38 | if (texts[key]) { 39 | msg += ` ${texts[key]}` 40 | console.log(msg) 41 | } 42 | } 43 | setTranscript(msg) 44 | }); 45 | 46 | realtimeTranscriber.current.on('error', event => { 47 | console.error(event); 48 | realtimeTranscriber.current.close(); 49 | realtimeTranscriber.current = null; 50 | }); 51 | 52 | realtimeTranscriber.current.on('close', (code, reason) => { 53 | console.log(`Connection closed: ${code} ${reason}`); 54 | realtimeTranscriber.current = null; 55 | }); 56 | 57 | await realtimeTranscriber.current.connect(); 58 | 59 | navigator.mediaDevices.getUserMedia({ audio: true }) 60 | .then((stream) => { 61 | recorder.current = new RecordRTC(stream, { 62 | type: 'audio', 63 | mimeType: 'audio/webm;codecs=pcm', 64 | recorderType: RecordRTC.StereoAudioRecorder, 65 | timeSlice: 250, 66 | desiredSampRate: 16000, 67 | numberOfAudioChannels: 1, 68 | bufferSize: 4096, 69 | audioBitsPerSecond: 128000, 70 | ondataavailable: async (blob) => { 71 | if(!realtimeTranscriber.current) return; 72 | const buffer = await blob.arrayBuffer(); 73 | realtimeTranscriber.current.sendAudio(buffer); 74 | }, 75 | }); 76 | recorder.current.startRecording(); 77 | }) 78 | .catch((err) => console.error(err)); 79 | 80 | setIsRecording(true) 81 | } 82 | 83 | const endTranscription = async (event) => { 84 | event.preventDefault(); 85 | setIsRecording(false) 86 | 87 | await realtimeTranscriber.current.close(); 88 | realtimeTranscriber.current = null; 89 | 90 | recorder.current.pauseRecording(); 91 | recorder.current = null; 92 | } 93 | 94 | 95 | return ( 96 |
97 |
98 |

Real-Time Transcription

99 |

Try AssemblyAI's new real-time transcription endpoint!

100 |
101 |
102 |

Click start to begin recording!

103 | {isRecording ? ( 104 | 105 | ) : ( 106 | 107 | )} 108 |
109 |
110 | {transcript} 111 |
112 |
113 | ); 114 | } 115 | 116 | export default App; 117 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/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 | --------------------------------------------------------------------------------