├── .gitignore ├── LICENSE ├── README.md ├── cli ├── .gitignore ├── README.md ├── index.js └── package.json ├── demo-placeholder.png ├── server ├── .gitignore ├── Dockerfile ├── index.js ├── index.starter.js ├── index.twilio.js ├── lib │ ├── assistant.js │ ├── audio.js │ ├── call │ │ ├── browser-vad.js │ │ ├── index.js │ │ └── twilio.js │ ├── conversation.js │ ├── platform │ │ └── twilio.js │ ├── stt.js │ └── tts.js ├── package-lock.json └── package.json └── web ├── .gitignore ├── README.md ├── config-overrides.js ├── package-lock.json ├── package.json ├── public ├── GitHub-Logo.svg ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── app.tsx ├── index.css ├── index.tsx ├── react-app-env.d.ts └── reportWebVitals.ts ├── tailwind.config.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Charles Yu (charlesyu108) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fullstack AI Voice Assistant Starter Project 2 | 3 | ### Demo Video 4 | [![Screenshot of the demo](./demo-placeholder.png)](https://www.youtube.com/watch?v=Cc38Z536suc) 5 | 6 | ### Try it yourself 7 | [https://voiceai-js-starter.vercel.app/](https://voiceai-js-starter.vercel.app/) 8 | 9 | ---------------------------------------------- 10 | 11 | This repo contains an end-to-end starter project for a simple websocket based AI voice assistant. 12 | I won't claim this project to be the fastest or most robust, but it's 100% free 13 | and open-source, so feel free to modify and use it as a base for your own projects. 14 | 15 | With fastest configurations, you can see assistant response times in ~1s TTFB (Time To First Byte) 16 | after the user stops speaking. 17 | 18 | If you have any questions or feedback, please feel free to open an issue. 19 | 20 | ## Features: 21 | - Drop-in support for several popular TTS / STT providers including (OpenAI, Deepgram, ElevenLabs, PlayHT) 22 | - Realtime audio streaming and playback via Websocket 23 | - Low-latency browser-based Voice Audio Detection 24 | - Interruptability 25 | - Function calling support (by default assistatnt call ending is supported) 26 | 27 | # Quick Start 28 | 29 | ## Option 1: npx create-voice-ai 30 | Simply run this command to create a new voice AI project. 31 | ``` 32 | npx create-voice-ai 33 | ``` 34 | 35 | ## Option 2: Clone this repo. 36 | 37 | ### Start Web-app: 38 | ``` 39 | cd web 40 | npm install 41 | npm run start 42 | ``` 43 | This will start the web-app on `localhost:3000` 44 | 45 | ### Start Server: 46 | You will need to create a `.env` file with the following environment variables. 47 | **With just an `OPENAI_API_KEY`, you can stand up a really powerful full-stack agent.** 48 | 49 | ```.env 50 | OPENAI_API_KEY= # Required 51 | ELEVEN_LABS_API_KEY= # For use with ElevenLabs 52 | DEEPGRAM_API_KEY= # For use with Deepgram 53 | PLAYHT_USER_ID= # For use with PlayHT 54 | PLAYHT_API_KEY= # For use with PlayHT 55 | ``` 56 | 57 | ``` 58 | cd server 59 | npm install 60 | npm run start 61 | ``` 62 | This will start the server on `localhost:8000` 63 | 64 | ## Your first configuration 65 | The toy example configured in this project is a simple ordering assistant for L&L Hawaiian BBQ. 66 | To create your own assistant, take a look at `server/index.starter.js` and follow the instructions. 67 | 68 | -------------------------------------------------------------------------------- /cli/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /cli/README.md: -------------------------------------------------------------------------------- 1 | # Create Voice AI 2 | 3 | `create-voice-ai` is a command-line tool that helps you quickly set up a new voice app using 4 | the [botany-labs/voice-ai-js-starter](https://github.com/botany-labs/voice-ai-js-starter) template. 5 | 6 | ## Usage 7 | ``` 8 | npx create-voice-ai 9 | ``` -------------------------------------------------------------------------------- /cli/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const { execSync } = require('child_process'); 3 | const chalk = require('chalk'); 4 | const inquirer = require('inquirer'); 5 | const fs = require('fs'); 6 | 7 | async function main() { 8 | console.log(chalk.green.bold("Welcome to the quickstart setup for botany-labs/voice-ai-js-starter!")); 9 | console.log(chalk.green("You're 30 seconds away from creating an amazing voice app.\n")); 10 | console.log(chalk.magenta("For more help, see https://github.com/botany-labs/voice-ai-js-starter\n")); 11 | 12 | const answers = await inquirer.prompt([ 13 | { 14 | type: 'input', 15 | name: 'directory', 16 | message: 'What directory would you like to create your voice app in?', 17 | default: './my-voice-app' 18 | }, 19 | { 20 | type: 'input', 21 | name: 'assistantFunction', 22 | message: 'What would you like your voice assistant to do? (Don\'t worry you can change this later)\n', 23 | default: 'You are a chatty AI assistant that makes lovely conversation about the weather.' 24 | }, 25 | { 26 | type: 'list', 27 | name: 'configuration', 28 | message: 'Which pre-made configuration would you like to use? (If you want to do more configuration, just choose easiest for now)', 29 | choices: [ 30 | { name: `Easy Setup (STT: OpenAI Whisper, TTS: OpenAI TTS-1, LLM: OpenAI ChatGPT 3.5 Turbo) ${chalk.bold('[RECOMMENDED]')}`, value: 'easy' }, 31 | { name: `Fastest Performance (STT: Deepgram Nova-2, TTS: Deepgram Aura, LLM: OpenAI ChatGPT 3.5 Turbo)`, value: 'fastest' }, 32 | { name: `Best Quality (STT: OpenAI Whisper, TTS: Eleven Labs Turbo V2, LLM: OpenAI ChatGPT 3.5 Turbo)`, value: 'best' } 33 | ] 34 | } 35 | ]); 36 | 37 | let apiKeys = {}; 38 | if (answers.configuration === 'easy') { 39 | apiKeys.openai = (await inquirer.prompt({ 40 | type: 'input', 41 | name: 'openai', 42 | message: `You selected ${chalk.magenta.bold('Easy Setup')}. This will require you to provide your ${chalk.yellow.bold('OpenAI API key')}:`, 43 | })).openai; 44 | } else if (answers.configuration === 'fastest') { 45 | apiKeys.openai = (await inquirer.prompt({ 46 | type: 'input', 47 | name: 'openai', 48 | message: `You selected ${chalk.magenta.bold('Fastest Performance')}. This will require you to provide your ${chalk.yellow.bold('OpenAI API key')}:`, 49 | })).openai; 50 | apiKeys.deepgram = (await inquirer.prompt({ 51 | type: 'input', 52 | name: 'deepgram', 53 | message: `You selected ${chalk.magenta.bold('Fastest Performance')}. This will require you to provide your ${chalk.yellow.bold('Deepgram API key')}:`, 54 | })).deepgram; 55 | } else if (answers.configuration === 'best') { 56 | apiKeys.openai = (await inquirer.prompt({ 57 | type: 'input', 58 | name: 'openai', 59 | message: `You selected ${chalk.magenta.bold('Best Quality')}. This will require you to provide your ${chalk.yellow.bold('OpenAI API key')}:`, 60 | })).openai; 61 | apiKeys.elevenLabs = (await inquirer.prompt({ 62 | type: 'input', 63 | name: 'elevenLabs', 64 | message: `You selected ${chalk.magenta.bold('Best Quality')}. This will require you to provide your ${chalk.yellow.bold('Eleven Labs API key')}:`, 65 | })).elevenLabs; 66 | } 67 | 68 | console.log(chalk.yellow("\nThanks for all that! Double check to confirm your settings:\n")); 69 | console.log(`${chalk.cyan.bold('Directory:')} ${answers.directory}`); 70 | console.log(`${chalk.cyan.bold('Assistant Function:')} ${answers.assistantFunction}`); 71 | console.log(`${chalk.cyan.bold('Configuration:')} ${answers.configuration}`); 72 | console.log(`${chalk.cyan.bold('API Keys:')} ${JSON.stringify(apiKeys, null, 2)}`); 73 | 74 | const confirm = await inquirer.prompt({ 75 | type: 'confirm', 76 | name: 'isCorrect', 77 | message: 'Is everything correct?', 78 | default: true 79 | }); 80 | 81 | if (!confirm.isCorrect) { 82 | console.log(chalk.red("Setup aborted. Please run the setup again.")); 83 | return; 84 | } 85 | 86 | console.log(chalk.green("\nGreat! One moment...\n")); 87 | 88 | // Make directory 89 | execSync(`mkdir -p ${answers.directory}`, { stdio: 'inherit' }); 90 | 91 | // Clone the web and server directories into the new directory 92 | execSync(`git clone https://github.com/botany-labs/voice-ai-js-starter/ ${answers.directory}`, { stdio: 'inherit' }); 93 | 94 | // Remove unnecessary files and directories from the cloned project 95 | clearUnnecessaryFilesAndDirectoriesFromClonedProject(answers.directory); 96 | 97 | // Make server dotenv file 98 | const contents = `# .env 99 | OPENAI_API_KEY=${apiKeys.openai} 100 | DEEPGRAM_API_KEY=${apiKeys.deepgram} 101 | ELEVENLABS_API_KEY=${apiKeys.elevenLabs} 102 | `; 103 | fs.writeFileSync(`${answers.directory}/server/.env`, contents); 104 | 105 | // Prepare index.js 106 | prepareIndexJS(answers.directory, answers.assistantFunction, preparedConfigurationToOptions[answers.configuration]); 107 | 108 | console.log(chalk.green("Installing dependencies...")); 109 | execSync(`cd ${answers.directory}/web && npm install`, { stdio: 'inherit' }); 110 | execSync(`cd ${answers.directory}/server && npm install`, { stdio: 'inherit' }); 111 | 112 | console.log(chalk.magenta.bold("\nSuccess!")); 113 | console.log(chalk.magenta.bold("\nNext, simply...")); 114 | console.log(chalk.green(`Run ${chalk.cyan.bold('npm run start')} in ${chalk.cyan.bold(`${answers.directory}/web`)} to start your client.`)); 115 | console.log(chalk.green(`Run ${chalk.cyan.bold('npm run start')} in ${chalk.cyan.bold(`${answers.directory}/server`)} to start your voice assistant server.`)); 116 | } 117 | 118 | main().catch(error => { 119 | console.error(chalk.red(error)); 120 | }); 121 | 122 | 123 | const prepareIndexJS = (dir, assistantPrompt, configuration) => { 124 | // In server/index.starter.js, there are two comment lines like this : // ---------------------------- 125 | // We need to replace the code between those lines with the configuration I provide here: 126 | 127 | const replacementContent =` 128 | const MyAssistant = new Assistant( 129 | "${assistantPrompt}", 130 | { 131 | llmModel: "${configuration.llmModel}", 132 | speechToTextModel: "${configuration.speechToTextModel}", 133 | voiceModel: "${configuration.voiceModel}", 134 | voiceName: "${configuration.voiceName}", 135 | } 136 | ); 137 | ` 138 | // Delete index.js 139 | fs.unlinkSync(`${dir}/server/index.js`); 140 | 141 | // Replace the code between the two comment lines with the replacementContent 142 | const indexJSContent = fs.readFileSync(`${dir}/server/index.starter.js`, 'utf8'); 143 | const indexJSContentLines = indexJSContent.split('\n'); 144 | const startIndex = indexJSContentLines.findIndex(line => line.includes('// ----------------------------')); 145 | const endIndex = indexJSContentLines.findIndex((line, idx) => idx > startIndex && line.includes('// ----------------------------')); 146 | const updatedIndexJSContent = indexJSContentLines.slice(0, startIndex).concat(replacementContent).concat(indexJSContentLines.slice(endIndex)).join('\n'); 147 | fs.writeFileSync(`${dir}/server/index.js`, updatedIndexJSContent); 148 | 149 | // Delete index.starter.js 150 | fs.unlinkSync(`${dir}/server/index.starter.js`); 151 | } 152 | 153 | 154 | const preparedConfigurationToOptions = { 155 | "easy": { 156 | llmModel: "gpt-3.5-turbo", 157 | speechToTextModel: "openai/whisper-1", 158 | voiceModel: "openai/tts-1", 159 | voiceName: "shimmer", 160 | }, 161 | "fastest": { 162 | llmModel: "gpt-3.5-turbo", 163 | speechToTextModel: "deepgram:live/nova-2", 164 | voiceModel: "deepgram/aura", 165 | voiceName: "asteria-en", 166 | }, 167 | "best": { 168 | llmModel: "gpt-3.5-turbo", 169 | speechToTextModel: "openai/whisper-1", 170 | voiceModel: "elevenlabs/eleven_turbo_v2", 171 | voiceName: "piTKgcLEGmPE4e6mEKli", 172 | } 173 | } 174 | 175 | const toKeepFromClone = [ 176 | "server", 177 | "web", 178 | ".gitignore", 179 | "LICENSE", 180 | "README.md", 181 | ] 182 | 183 | const clearUnnecessaryFilesAndDirectoriesFromClonedProject = (dir) => { 184 | // List directories and files in the cloned project 185 | const filesAndDirectories = fs.readdirSync(dir); 186 | // Delete all files and directories that are not in toKeepFromClone 187 | filesAndDirectories.forEach(fileOrDirectory => { 188 | const filePath = `${dir}/${fileOrDirectory}`; 189 | const stats = fs.lstatSync(filePath); 190 | if (!toKeepFromClone.includes(fileOrDirectory)) { 191 | if (stats.isDirectory()) { 192 | execSync(`rm -rf ${filePath}`, { stdio: 'inherit' }); 193 | } else { 194 | fs.unlinkSync(filePath); 195 | } 196 | } 197 | }); 198 | } 199 | 200 | -------------------------------------------------------------------------------- /cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-voice-ai", 3 | "version": "0.0.7", 4 | "description": "Quickly bootstrap an AI-voice assistant", 5 | "main": "index.js", 6 | "bin": { 7 | "create-voice-app": "./index.js", 8 | "create-voice-ai": "./index.js" 9 | }, 10 | "scripts": { 11 | "start": "node index.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/botany-labs/voice-ai-js-starter.git" 16 | }, 17 | "author": "Charles Yu", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/botany-labs/voice-ai-js-starter/issues" 21 | }, 22 | "homepage": "https://github.com/botany-labs/voice-ai-js-starter#readme", 23 | "dependencies": { 24 | "chalk": "^4.1.2", 25 | "inquirer": "^8.0.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /demo-placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/botany-labs/voice-ai-js-starter/2640781dee8faf68e8079387cf5785ae7ba07807/demo-placeholder.png -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20 2 | 3 | WORKDIR /app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | EXPOSE 8000 12 | 13 | CMD ["node", "index.js"] 14 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const dotenv = require("dotenv"); 2 | dotenv.config(); 3 | 4 | const WebSocket = require("ws"); 5 | const { Assistant } = require("./lib/assistant"); 6 | 7 | const PORT = 8000; 8 | 9 | const server = new WebSocket.Server({ port: PORT }); 10 | 11 | const LnlCustomerSupport_Fastest = new Assistant( 12 | ` You are a delightful AI voice agent for L-n-L Hawaiian Barbecue catering in Milbrae CA off El Camino. 13 | You are receiving a call from a customer. 14 | Please be polite but concise. Respond ONLY with the text to be spoken. DO NOT add any prefix. 15 | 16 | If they are placing an order, make sure to take down contact info, the order, and give them the price before they hang up. 17 | You must fully address the customer's inquiry and give a polite goodbye when you hang up the call. 18 | If the user has already said bye, just hang up.`, 19 | { 20 | speakFirstOpeningMessage: "L-n-L Hawaiian Barbecue, El Camino. How can I help you today?", 21 | llmModel: "gpt-3.5-turbo", 22 | speechToTextModel: "deepgram:live/nova-2", 23 | voiceModel: "deepgram/aura", 24 | voiceName: "asteria-en", 25 | } 26 | ); 27 | 28 | const LnlCustomerSupport_BestQuality = new Assistant( 29 | ` You are a delightful AI voice agent for L-n-L Hawaiian Barbecue catering in Milbrae CA off El Camino. 30 | You are receiving a call from a customer. 31 | Please be polite but concise. Respond ONLY with the text to be spoken. DO NOT add any prefix. 32 | You are configured with a multi-lingual TTS. Feel free to respond back in the language of the customer. 33 | 34 | If they are placing an order, make sure to take down contact info, the order, and give them the price before they hang up. 35 | You must fully address the customer's inquiry and give a polite goodbye when you hang up the call. 36 | If the user has already said bye, just hang up.`, 37 | { 38 | speakFirstOpeningMessage: "L-n-L Hawaiian Barbecue, El Camino. How can I help you today?", 39 | llmModel: "gpt-3.5-turbo", 40 | speechToTextModel: "openai/whisper-1", 41 | voiceModel: "elevenlabs/eleven_turbo_v2", 42 | voiceName: "piTKgcLEGmPE4e6mEKli", 43 | } 44 | ) 45 | 46 | const LnlCustomerSupport_OpenAI = new Assistant( 47 | ` You are a delightful AI voice agent for L-n-L Hawaiian Barbecue catering in Milbrae CA off El Camino. 48 | You are receiving a call from a customer. 49 | Please be polite but concise. Respond ONLY with the text to be spoken. DO NOT add any prefix. 50 | You are configured with a multi-lingual TTS. Feel free to respond back in the language of the customer. 51 | 52 | If they are placing an order, make sure to take down contact info, the order, and give them the price before they hang up. 53 | You must fully address the customer's inquiry and give a polite goodbye when you hang up the call. 54 | If the user has already said bye, just hang up.`, 55 | { 56 | speakFirstOpeningMessage: "L-n-L Hawaiian Barbecue, El Camino. How can I help you today?", 57 | llmModel: "gpt-3.5-turbo", 58 | } 59 | ); 60 | 61 | const LnlCustomerSupport_Default = LnlCustomerSupport_Fastest; 62 | 63 | server.on("connection", (ws, req) => { 64 | const cid = req.headers["sec-websocket-key"]; 65 | ws.binaryType = "arraybuffer"; 66 | 67 | // resolve query 68 | const query = req.url.split("?")[1]; 69 | const queryParams = new URLSearchParams(query); 70 | const assistant = queryParams.get("assistant"); 71 | 72 | 73 | const LnlCustomerSupport = ( 74 | assistant === "fastest" ? LnlCustomerSupport_Fastest : 75 | assistant === "best-quality" ? LnlCustomerSupport_BestQuality : 76 | assistant === "openai" ? LnlCustomerSupport_OpenAI : 77 | LnlCustomerSupport_Default 78 | ); 79 | 80 | ws.send(`--- Configured to use ${(assistant ?? 'DEFAULT').toUpperCase()} assistant ---`); 81 | 82 | let demoTimeout; 83 | if (process.env.IS_DEMO) { 84 | const timeoutMinutes = 2; 85 | const timeoutMs = timeoutMinutes * 60 * 1000; 86 | demoTimeout = setTimeout(() => { 87 | ws.send("---- FORCED CALL END ----"); 88 | ws.send(`---- Timed out because demo time limit was reached (${timeoutMinutes} minutes) ----`); 89 | ws.close(); 90 | }, timeoutMs); 91 | } 92 | 93 | // To have an AI agent talk to the user we just need to create a conversation and begin it. 94 | // The conversation will handle the audio streaming and the AI agent will handle the text streaming. 95 | const conversation = LnlCustomerSupport.createConversation(ws, { 96 | onEnd: (callLogs) => { 97 | console.log("----- CALL LOG -----"); 98 | console.log(callLogs); 99 | }, 100 | }); 101 | conversation.begin(2000); 102 | 103 | ws.on("close", () => { 104 | clearTimeout(demoTimeout); 105 | console.log("Client disconnected", cid); 106 | }); 107 | 108 | ws.on("error", (error) => { 109 | console.error(`WebSocket error: ${error}`); 110 | }); 111 | }); 112 | 113 | console.log(`WebSocket server is running on ws://localhost:${PORT}`); 114 | -------------------------------------------------------------------------------- /server/index.starter.js: -------------------------------------------------------------------------------- 1 | // This is a bare-bones example that you can use to get started with implementing your own assistant. 2 | // Simply delete the index.js and rename this file to index.js and you're good to go! 3 | // You can connect to this using the existing demo web UI (the pre-configured settings on the UI won't do anything though). 4 | 5 | const WebSocket = require("ws"); 6 | const dotenv = require("dotenv"); 7 | const { Assistant } = require("./lib/assistant"); 8 | 9 | dotenv.config(); 10 | 11 | const PORT = 8000; 12 | 13 | const server = new WebSocket.Server({ port: PORT }); 14 | 15 | // ---------------------------- 16 | const MyAssistant = new Assistant( 17 | `TODO: `, 18 | { 19 | speakFirstOpeningMessage: "TODO: ", 20 | } 21 | ); 22 | // ---------------------------- 23 | 24 | server.on("connection", (ws, req) => { 25 | const cid = req.headers["sec-websocket-key"]; 26 | ws.binaryType = "arraybuffer"; 27 | 28 | 29 | // To have an AI agent talk to the user we just need to create a conversation and begin it. 30 | // The conversation will handle the audio streaming and the AI agent will handle the text streaming. 31 | 32 | const conversation = MyAssistant.createConversation(ws, { 33 | onEnd: (callLogs) => { 34 | console.log("----- CALL LOG -----"); 35 | console.log(callLogs); 36 | }, 37 | }); 38 | conversation.begin(2000); 39 | 40 | ws.on("close", () => { 41 | console.log("Client disconnected", cid); 42 | }); 43 | 44 | ws.on("error", (error) => { 45 | console.error(`WebSocket error: ${error}`); 46 | }); 47 | }); 48 | 49 | console.log(`WebSocket server is running on ws://localhost:${PORT}`); 50 | -------------------------------------------------------------------------------- /server/index.twilio.js: -------------------------------------------------------------------------------- 1 | // This is a starter that sets up a Twilio Call Center 2 | // NOTE: You will need to use the Twilio API to invoke a call 3 | // 4 | // Ex via CLI: 5 | // twilio api:core:calls:create --from --to --url "/twiml" 6 | // 7 | const dotenv = require("dotenv"); 8 | dotenv.config(); 9 | 10 | const { TwilioCallServer } = require("./lib/platform/twilio"); 11 | const { Assistant } = require("./lib/assistant"); 12 | const { Conversation } = require("./lib/conversation"); 13 | const { TwilioCall } = require("./lib/call/twilio"); 14 | const { TTS_AUDIO_FORMATS } = require("./lib/tts"); 15 | 16 | const PORT = process.env.PORT ?? 8000; 17 | const HOST = process.env.SERVER_HOST ?? `localhost:${PORT}`; 18 | 19 | const LnlCustomerSupport = new Assistant( 20 | ` You are a delightful AI voice agent for L-n-L Hawaiian Barbecue catering in Milbrae CA off El Camino. 21 | You are receiving a call from a customer. 22 | Please be polite but concise. Respond ONLY with the text to be spoken. DO NOT add any prefix. 23 | 24 | If they are placing an order, make sure to take down contact info, the order, and give them the price before they hang up. 25 | You must fully address the customer's inquiry and give a polite goodbye when you hang up the call. 26 | If the user has already said bye, just hang up.`, 27 | { 28 | speakFirstOpeningMessage: "L-n-L Hawaiian Barbecue, El Camino. How can I help you today?", 29 | llmModel: "gpt-3.5-turbo", 30 | voiceModel: "deepgram/aura", 31 | voiceName: "asteria-en", 32 | ttsFormat: TTS_AUDIO_FORMATS.MULAW_8K 33 | } 34 | ); 35 | 36 | const onConnect = (ws) => { 37 | console.log("Connected to Twilio"); 38 | const call = new TwilioCall(ws); 39 | const conversation = new Conversation(LnlCustomerSupport, call); 40 | conversation.begin(); 41 | } 42 | 43 | 44 | const CallCenter = new TwilioCallServer(HOST, onConnect); 45 | CallCenter.serve(PORT); -------------------------------------------------------------------------------- /server/lib/assistant.js: -------------------------------------------------------------------------------- 1 | const { TextToSpeech, TTS_AUDIO_FORMATS } = require ('./tts'); 2 | const { SpeechToText } = require ('./stt'); 3 | const { Conversation } = require("./conversation"); 4 | const { BrowserVADWebCall } = require("./call/browser-vad"); 5 | const OpenAI = require("openai"); 6 | const openai = new OpenAI({ 7 | apiKey: process.env.OPENAI_API_KEY, 8 | }); 9 | 10 | /** 11 | * Defines an AI call assistant. 12 | * 13 | * TODO: Support custom tools 14 | */ 15 | class Assistant { 16 | /** 17 | * @param {string} instructions - Instructions to give your assistant. 18 | * @param {object} [options] - Options to give your assistant. 19 | * @param {string} [options.llmModel] - LLM model to use. Defaults to "gpt-3.5-turbo". 20 | * @param {string} [options.voiceModel] - Voice model to use. Defaults to "openai/tts-1". See TTS_MODELS (./speech.js) for supported models. 21 | * @param {string} [options.voiceName] - Voice name to use. Defaults to "shimmer". 22 | * @param {string} [options.ttsFormat] - TTS format to use. Defaults to TTS_AUDIO_FORMATS.PCM_24K. 23 | * @param {string} [options.speechToTextModel] - Speech-to-text model to use. Defaults to "openai/whisper-1". See SPEECH_TO_TEXT_MODELS (./speech.js) for supported models. 24 | * @param {string} [options.systemPrompt] - System prompt to give your assistant. 25 | * @param {string} [options.speakFirstOpeningMessage] - Opening message to give your assistant to say once the call starts. If not provided, the assistant will just be prompted to speak. 26 | * @param {string} [options.speakFirst] - Speak first? Defaults to true. 27 | * @param {string} [options.canHangUp] - Can hang up? Defaults to true. 28 | */ 29 | constructor(instructions, options = {}) { 30 | this.instructions = instructions; 31 | this.systemPrompt = options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT; 32 | this.tools = options.canHangUp === false ? TOOLS_NONE : TOOL_HANG_UP; // NOTE: only tool supported right now is hang-up 33 | this.speakFirst = options.speakFirst ?? true; 34 | this.speakFirstOpeningMessage = options.speakFirstOpeningMessage; 35 | this.llmModel = options.llmModel ?? "gpt-3.5-turbo"; 36 | this.voiceModel = options.voiceModel ?? "openai/tts-1"; 37 | this.voiceName = options.voiceName ?? "shimmer"; 38 | this.speechToTextModel = options.speechToTextModel ?? "openai/whisper-1"; 39 | this.tts = new TextToSpeech(this.voiceModel, this.voiceName, options.ttsFormat ?? TTS_AUDIO_FORMATS.PCM_24K); 40 | } 41 | 42 | /** 43 | * Assembles the prompt for chat LLMs. 44 | * @param {string} systemPrompt - System prompt to give your assistant. 45 | * @param {string} providedInstructions - Instructions to give your assistant. 46 | * @param {string} tools - Tools to give your assistant. 47 | */ 48 | _assemblePrompt(systemPrompt, providedInstructions, tools) { 49 | let instructionPrompt = INSTRUCTION_PROMPT_BASE; 50 | instructionPrompt = instructionPrompt.replace( 51 | "{instructions}", 52 | providedInstructions 53 | ); 54 | instructionPrompt = instructionPrompt.replace("{tools}", tools); 55 | 56 | const prompt = [ 57 | { 58 | role: "system", 59 | content: systemPrompt, 60 | }, 61 | { 62 | role: "user", 63 | content: instructionPrompt, 64 | }, 65 | ]; 66 | 67 | return prompt; 68 | } 69 | 70 | get prompt() { 71 | return this._assemblePrompt( 72 | this.systemPrompt, 73 | this.instructions, 74 | this.tools 75 | ); 76 | } 77 | 78 | /** 79 | * @param {object[]} conversation - Chat conversation to create a response for. 80 | */ 81 | async createResponse(conversation) { 82 | let selectedTool = undefined; 83 | 84 | const response = await openai.chat.completions.create({ 85 | model: "gpt-3.5-turbo", 86 | messages: conversation, 87 | }); 88 | 89 | let content = response.choices[0].message.content; 90 | 91 | if (content.includes("[endCall]")) { 92 | content = content.replace("[endCall]", ""); 93 | return { 94 | content, 95 | selectedTool: "endCall", 96 | }; 97 | } 98 | 99 | return { 100 | content, 101 | selectedTool, 102 | }; 103 | } 104 | 105 | async textToSpeech(content) { 106 | const result = await this.tts.synthesize(content); 107 | return result; 108 | } 109 | 110 | // Create a conversation with this assistant 111 | /** 112 | * @param {WebSocket} ws - WebSocket to create a conversation with. 113 | * @param {object} [options] - Options to give your assistant. 114 | */ 115 | createConversation(ws, options={}) { 116 | const stt = new SpeechToText(this.speechToTextModel); 117 | const call = new BrowserVADWebCall(ws, stt); 118 | return new Conversation(this, call, options); 119 | } 120 | } 121 | 122 | // ----- Prompting ------ 123 | 124 | const DEFAULT_SYSTEM_PROMPT = 125 | `You are a delightful AI voice agent. 126 | Please be polite but concise. 127 | Show a bit of personality. 128 | Your text will be passed to a Text-To-Speech model. 129 | Please respond with an answer that is going to be transcribed well and add uhs, ums, mhms, and other disfluencies as needed to keep it casual. 130 | Respond ONLY with the text to be spoken. 131 | DO NOT add any prefix. 132 | The dialogue is transcribed and might be a bit wrong if the speech to text is bad. 133 | Don't be afraid to ask to clarify if you don't understand what the customer said because you may have misheard.`; 134 | 135 | const INSTRUCTION_PROMPT_BASE = ` 136 | INSTRUCTIONS 137 | {instructions} 138 | 139 | TOOLS 140 | {tools} 141 | `; 142 | 143 | const TOOL_HANG_UP = 144 | "[endCall] : You can use the token [endCall] tool to hang up the call. Write it exactly as that."; 145 | const TOOLS_NONE = "N/A"; 146 | 147 | exports.Assistant = Assistant; 148 | -------------------------------------------------------------------------------- /server/lib/audio.js: -------------------------------------------------------------------------------- 1 | // raw audio mangling 2 | 3 | function float32ToPCM16(buffer) { 4 | const pcm16 = new Int16Array(buffer.length); 5 | for (let i = 0; i < buffer.length; i++) { 6 | let s = Math.max(-1, Math.min(1, buffer[i])); 7 | pcm16[i] = s < 0 ? s * 32768 : s * 32767; 8 | } 9 | return pcm16; 10 | } 11 | 12 | function pcm16ToFloat32(buffer) { 13 | const float32 = new Float32Array(buffer.length); 14 | for (let i = 0; i < buffer.length; i++) { 15 | float32[i] = buffer[i] / 32767; 16 | } 17 | return float32; 18 | } 19 | 20 | function createWavHeader(sampleRate, numChannels, bytesPerSample, dataSize) { 21 | const blockAlign = numChannels * bytesPerSample; 22 | const byteRate = sampleRate * blockAlign; 23 | 24 | const buffer = new ArrayBuffer(44); 25 | const view = new DataView(buffer); 26 | 27 | // RIFF identifier 28 | view.setUint32(0, 1380533830, false); // 'RIFF' 29 | 30 | // file length minus RIFF identifier and file type header 31 | view.setUint32(4, 36 + dataSize, true); 32 | // RIFF type 33 | view.setUint32(8, 1463899717, false); // 'WAVE' 34 | 35 | // format chunk identifier 36 | view.setUint32(12, 1718449184, false); // 'fmt ' 37 | 38 | // format chunk length 39 | view.setUint32(16, 16, true); 40 | // sample format (raw) 41 | view.setUint16(20, 1, true); 42 | // channel count 43 | view.setUint16(22, numChannels, true); 44 | // sample rate 45 | view.setUint32(24, sampleRate, true); 46 | // byte rate (sample rate * block align) 47 | view.setUint32(28, byteRate, true); 48 | // block align (channel count * bytes per sample) 49 | view.setUint16(32, blockAlign, true); 50 | // bits per sample 51 | view.setUint16(34, bytesPerSample * 8, true); 52 | // data chunk identifier 53 | view.setUint32(36, 1684108385, false); // 'data' 54 | 55 | // data chunk length 56 | view.setUint32(40, dataSize, true); 57 | 58 | return Buffer.from(buffer); 59 | } 60 | 61 | function generateBeep(frequency, durationSeconds, sampleRate) { 62 | const volume = 0.07; 63 | const numSamples = sampleRate * durationSeconds; 64 | const buffer = new Float32Array(numSamples); 65 | for (let i = 0; i < numSamples; i++) { 66 | buffer[i] = Math.sin(2 * Math.PI * frequency * (i / sampleRate)) * volume; 67 | } 68 | return float32ToPCM16(buffer); 69 | } 70 | 71 | function float32_pcm16ToWavBlob(float32_pcm16) { 72 | let pcm16 = float32ToPCM16(float32_pcm16); 73 | const sampleRate = 24000; 74 | const bitDepth = 16; 75 | const numChannels = 1; 76 | 77 | // Create WAV header 78 | const wavHeader = createWavHeader( 79 | sampleRate, 80 | numChannels, 81 | bitDepth / 8, 82 | pcm16.length * 2 83 | ); 84 | 85 | // Concatenate header and PCM data 86 | const wavBuffer = Buffer.concat([wavHeader, Buffer.from(pcm16.buffer)]); 87 | 88 | // Create a Blob from the WAV buffer 89 | const wavBlob = new Blob([wavBuffer], { type: "audio/wav" }); 90 | wavBlob.name = "audio.wav"; 91 | wavBlob.lastModified = Date.now(); 92 | return wavBlob; 93 | } 94 | 95 | module.exports = { 96 | float32_pcm16ToWavBlob, 97 | float32ToPCM16, 98 | pcm16ToFloat32, 99 | createWavHeader, 100 | generateBeep, 101 | }; 102 | -------------------------------------------------------------------------------- /server/lib/call/browser-vad.js: -------------------------------------------------------------------------------- 1 | // Implementation of Call for a browser client that uses a browser-side VAD. 2 | 3 | const { SpeechToText } = require("../stt"); 4 | const { generateBeep } = require("../audio"); 5 | const { Call, CallEvents } = require("../call"); 6 | const { pcm16ToFloat32 } = require("../audio"); 7 | 8 | const BrowserVADWebCallEvents = { 9 | SERVER_START_LISTENING: "RDY", 10 | CLIENT_END_OF_SPEECH: "EOS", 11 | CLIENT_INTERRUPT: "INT", 12 | SERVER_REQUEST_CLEAR_BUFFER: "CLR", 13 | }; 14 | 15 | /** 16 | * BrowserVADWebCall is an implementation of Call for a browser client that uses 17 | * a browser-side VAD implementation. 18 | * */ 19 | class BrowserVADWebCall extends Call { 20 | 21 | /** 22 | * @param {WebSocket} ws - Websocket to use for the call. 23 | * @param {SpeechToText} stt - Speech to text instance to use for the call. 24 | */ 25 | constructor(ws, stt) { 26 | super(); 27 | this.ws = ws; 28 | this.stt = stt; 29 | this.pendingSamples = []; 30 | this.ws.on("message", this._onWebsocketMessage.bind(this)); 31 | this.ws.on("close", () => { 32 | this.emit(CallEvents.CALL_ENDED); 33 | this.stt.destroy(); 34 | }); 35 | this.pushAudio(generateBeep(440, 0.5, 24000)); 36 | } 37 | 38 | async pushAudio(raw_audio_as_pcm) { 39 | const pcm = new Int16Array(raw_audio_as_pcm); 40 | const audio_float32_24k_16bit_1channel = pcm16ToFloat32(pcm); 41 | for (let i = 0; i < audio_float32_24k_16bit_1channel.length; i += 1024) { 42 | this.ws.send(audio_float32_24k_16bit_1channel.slice(i, i + 1024)); 43 | } 44 | } 45 | 46 | async pushMeta(metadata) { 47 | this.ws.send(metadata); 48 | } 49 | 50 | async indicateReady() { 51 | this.pushMeta("--- Assistant Ready ---"); 52 | this.pushMeta(BrowserVADWebCallEvents.SERVER_START_LISTENING); 53 | } 54 | 55 | async end() { 56 | this.pushAudio(generateBeep(180, 0.5, 24000)); 57 | this.ws.close(); 58 | } 59 | 60 | async _onWebsocketMessage(message) { 61 | if (message instanceof ArrayBuffer) { 62 | this._handleWebsocket_Audio(message); 63 | } else { 64 | this._handleWebsocket_Meta(message); 65 | } 66 | } 67 | 68 | async _handleWebsocket_Meta(message) { 69 | let messageString = message.toString(); 70 | if (messageString === BrowserVADWebCallEvents.CLIENT_END_OF_SPEECH) { 71 | if (this.pendingSamples.length) { 72 | const data = this.pendingSamples.slice(); 73 | const combinedAudio = new Float32Array(data.length * 1024); 74 | for (let i = 0; i < data.length; i++) { 75 | combinedAudio.set(data[i], i * 1024); 76 | } 77 | const transcription = await this.stt.transcribe(combinedAudio); 78 | this.pushMeta(BrowserVADWebCallEvents.SERVER_REQUEST_CLEAR_BUFFER); 79 | this.emit(CallEvents.USER_MESSAGE, transcription); 80 | this.pendingSamples = []; 81 | } else { 82 | console.warn("GOT EOS but no audio"); 83 | } 84 | return; 85 | } 86 | 87 | if (messageString === BrowserVADWebCallEvents.CLIENT_INTERRUPT) { 88 | this.emit(CallEvents.INTERRUPT); 89 | return; 90 | } 91 | 92 | console.error("Unknown message type:", message); 93 | } 94 | 95 | async _handleWebsocket_Audio(message) { 96 | const audio = new Float32Array(message); 97 | this.pendingSamples.push(audio); 98 | return; 99 | } 100 | } 101 | 102 | module.exports = { 103 | BrowserVADWebCall, 104 | BrowserVADWebCallEvents 105 | }; -------------------------------------------------------------------------------- /server/lib/call/index.js: -------------------------------------------------------------------------------- 1 | const { EventEmitter } = require("events"); 2 | 3 | const CallEvents = { 4 | USER_MESSAGE: "userMessage", 5 | CALL_ENDED: "callEnded", 6 | INTERRUPT: "interrupt", 7 | }; 8 | 9 | class Call extends EventEmitter { 10 | constructor() { 11 | super(); 12 | } 13 | 14 | async indicateReady() { 15 | throw new Error("Not implemented"); 16 | } 17 | 18 | async pushAudio(audioBuffer) { 19 | throw new Error("Not implemented"); 20 | } 21 | 22 | async pushMeta(metadata) { 23 | throw new Error("Not implemented"); 24 | } 25 | 26 | async end() { 27 | throw new Error("Not implemented"); 28 | } 29 | } 30 | 31 | module.exports = { Call, CallEvents }; -------------------------------------------------------------------------------- /server/lib/call/twilio.js: -------------------------------------------------------------------------------- 1 | const { Call, CallEvents } = require("../call"); 2 | const { 3 | createClient: Deepgram, 4 | LiveTranscriptionEvents, 5 | } = require("@deepgram/sdk"); 6 | 7 | class TwilioCall extends Call { 8 | /** 9 | * @param {Object} ws - WebSocket connection to the call 10 | */ 11 | constructor(ws) { 12 | super(); 13 | this.ws = ws; 14 | this.pendingSamples = []; 15 | this.pendingTranscribedMessages = []; 16 | this.streamSid = null; 17 | this.connectionOpen = false; 18 | this.ws.on("message", this._onWebsocketMessage.bind(this)); 19 | this.ws.on("close", () => { 20 | this.emit("callEnded"); 21 | this.transcriber.finish(); 22 | }); 23 | 24 | this.transcriber = this.setUpTranscriber(); 25 | this.userIsSpeaking = false; 26 | this.assistantIsSpeaking = false; 27 | this.assistantSpeakingTimer = null; 28 | this.assistantInterruptionTimer = null; 29 | } 30 | 31 | async _onWebsocketMessage(message) { 32 | if (message.type === "utf8") { 33 | var data = JSON.parse(message.utf8Data); 34 | 35 | if (!this.streamSid) { 36 | this.streamSid = data.streamSid; 37 | console.log("StreamSid: ", this.streamSid); 38 | } 39 | 40 | if (data.event === "media") { 41 | this.pendingSamples.push(data.media.payload); 42 | this._onMediaMessage(); 43 | } 44 | // ------------------------------------------------- 45 | if (data.event === "connected") { 46 | console.log("From Twilio: Connected event received: ", data); 47 | } 48 | if (data.event === "start") { 49 | console.log("From Twilio: Start event received: ", data); 50 | } 51 | if (data.event === "mark") { 52 | console.log("From Twilio: Mark event received", data); 53 | } 54 | if (data.event === "close") { 55 | console.log("From Twilio: Close event received: ", data); 56 | this.end(); 57 | } 58 | } else if (message.type === "binary") { 59 | console.log("From Twilio: binary message received (not supported)"); 60 | } 61 | } 62 | 63 | async _onMediaMessage() { 64 | if (this.pendingSamples.length < 1 || !this.connectionOpen) { 65 | return; 66 | } 67 | const pendingByteBuffers = this.pendingSamples.map((sample) => { 68 | return Buffer.from(sample, "base64"); 69 | }); 70 | 71 | const combined = Buffer.concat(pendingByteBuffers); 72 | this.transcriber.send(combined); 73 | this.pendingSamples = []; 74 | } 75 | 76 | setUpTranscriber() { 77 | const deepgram = Deepgram(process.env.DEEPGRAM_API_KEY); 78 | const connection = deepgram.listen.live({ 79 | model: "nova-2", 80 | encoding: "mulaw", 81 | sample_rate: 8000, 82 | smart_format: false, 83 | interim_results: true, 84 | numerals: true, 85 | endpointing: 200, 86 | vad_events: true, 87 | interim_results: true, 88 | utterance_end_ms: "1000", 89 | }); 90 | 91 | connection.on(LiveTranscriptionEvents.Open, () => { 92 | console.log("Deepgram Live Connection opened."); 93 | this.connectionOpen = true; 94 | }); 95 | 96 | connection.on(LiveTranscriptionEvents.Transcript, (data) => { 97 | if (!data.is_final) { 98 | return; 99 | } 100 | this.userIsSpeaking = true; 101 | const transcript = data.channel.alternatives.reduce((acc, alt) => { 102 | return acc + alt.transcript; 103 | }, ""); 104 | 105 | if (data.speech_final) { 106 | console.log("Speech final event received", transcript); 107 | } 108 | this.pendingTranscribedMessages.push(transcript); 109 | }); 110 | 111 | connection.on(LiveTranscriptionEvents.SpeechStarted, () => { 112 | console.log("Speech started event received"); 113 | this._checkForInterruptions(); 114 | }); 115 | 116 | connection.on(LiveTranscriptionEvents.UtteranceEnd, () => { 117 | this.userIsSpeaking = false; 118 | const joined = this.pendingTranscribedMessages.join(" "); 119 | this.pendingTranscribedMessages = []; 120 | this.emit("userMessage", joined); 121 | }); 122 | 123 | connection.on(LiveTranscriptionEvents.Close, () => { 124 | this.userIsSpeaking = false; 125 | this.connectionOpen = false; 126 | console.log("Deepgram Live Connection closed."); 127 | }); 128 | 129 | connection.on(LiveTranscriptionEvents.Error, (err) => { 130 | console.error(err); 131 | }); 132 | 133 | return connection; 134 | } 135 | 136 | async pushAudio(audioBuffer) { 137 | await this._clearClientAudio(); 138 | const markName = `audio-${Date.now()}`; 139 | this._updateSpeakingTracking(audioBuffer); 140 | const payload = Buffer.from(audioBuffer).toString("base64"); 141 | const message = { 142 | event: "media", 143 | streamSid: this.streamSid, 144 | media: { 145 | payload: payload, 146 | }, 147 | }; 148 | 149 | const messageJSON = JSON.stringify(message); 150 | await this.ws.sendUTF(messageJSON); 151 | await this.ws.sendUTF( 152 | JSON.stringify({ 153 | event: "mark", 154 | streamSid: this.streamSid, 155 | mark: { name: markName }, 156 | }) 157 | ); 158 | } 159 | 160 | async _clearClientAudio() { 161 | await this.ws.sendUTF( 162 | JSON.stringify({ event: "clear", streamSid: this.streamSid }) 163 | ); 164 | } 165 | 166 | _updateSpeakingTracking(audioBuffer) { 167 | const expectedDuration = this._computeAudioDuration(audioBuffer); 168 | console.log("Expected Audio duration is", expectedDuration, "seconds"); 169 | this.assistantIsSpeaking = true; 170 | clearTimeout(this.assistantSpeakingTimer); 171 | 172 | this.assistantSpeakingTimer = setTimeout(() => { 173 | console.log("Assistant is done speaking"); 174 | this.assistantIsSpeaking = false; 175 | }, expectedDuration * 1000); 176 | } 177 | 178 | _computeAudioDuration(audioBuffer) { 179 | return audioBuffer.length / 8000; 180 | } 181 | 182 | _checkForInterruptions() { 183 | clearTimeout(this.assistantInterruptionTimer); 184 | if (this.assistantIsSpeaking) { 185 | this.assistantInterruptionTimer = setTimeout(() => { 186 | if (this.assistantIsSpeaking && this.userIsSpeaking) { 187 | console.log("[Interruption detected!]"); 188 | this.emit(CallEvents.INTERRUPT); 189 | this._clearClientAudio(); 190 | this.assistantIsSpeaking = false; 191 | } 192 | }, 2000); 193 | } 194 | } 195 | 196 | async pushMeta(metadata) { 197 | // No-Op 198 | } 199 | 200 | async indicateReady() { 201 | // No-Op 202 | } 203 | 204 | async end() { 205 | this.emit(CallEvents.END); 206 | this.ws.close(); 207 | } 208 | } 209 | 210 | module.exports = { TwilioCall }; 211 | -------------------------------------------------------------------------------- /server/lib/conversation.js: -------------------------------------------------------------------------------- 1 | const { CallEvents } = require("./call"); 2 | 3 | /** 4 | * Conversation represents a conversation between a user and an assistant. 5 | * It listens for user messages, sends assistant messages, and handles tool selections. 6 | */ 7 | class Conversation { 8 | /** 9 | * Constructor 10 | * @param {Assistant} assistant - Assistant to use for the conversation. 11 | * @param {Call} call - Call to use for the conversation. 12 | * @param {object} options - Options for the conversation. 13 | * @param {(callLogs: Array<{timestamp: string, event: string, meta: object}>) => void} options.onEnd - Function to call when the conversation ends. 14 | */ 15 | constructor(assistant, call, stt, options = {}) { 16 | this.assistant = assistant; 17 | this.call = call; 18 | this.onEnd = options.onEnd || (() => { }); 19 | this.history = assistant.prompt; 20 | this.callLog = []; 21 | this.call.on(CallEvents.CALL_ENDED, () => { 22 | this.addToCallLog("CALL_ENDED"); 23 | this.onEnd && this.onEnd(this.callLog); 24 | }); 25 | this.call.on(CallEvents.INTERRUPT, () => { 26 | this.noteWhatWasSaid("user", "[Interrupted your last message]"); 27 | }); 28 | this.addToCallLog("INIT", { 29 | assistant: JSON.stringify(this.assistant), 30 | }); 31 | } 32 | 33 | /** 34 | * Begins the conversation. 35 | * @param {number} delay - Delay in milliseconds before starting to listen for user messages. 36 | */ 37 | async begin(delay = 0) { 38 | setTimeout(async () => { 39 | this.startListening(); 40 | this.addToCallLog("READY"); 41 | this.call.indicateReady(); 42 | 43 | if (this.assistant.speakFirst) { 44 | let firstMessage = this.assistant.speakFirstOpeningMessage; 45 | if (!firstMessage) { 46 | const { content } = await this.assistant.createResponse(this.history); 47 | firstMessage = content; 48 | } 49 | this.noteWhatWasSaid("assistant", firstMessage); 50 | const audio = await this.assistant.textToSpeech(firstMessage); 51 | this.call.pushAudio(audio); 52 | } 53 | }, delay); 54 | } 55 | 56 | /** 57 | * Starts listening for user messages. 58 | */ 59 | startListening() { 60 | this.call.on(CallEvents.USER_MESSAGE, async (message) => { 61 | this.noteWhatWasSaid("user", message); 62 | const { content, selectedTool } = await this.assistant.createResponse( 63 | this.history 64 | ); 65 | if (content) { 66 | this.noteWhatWasSaid("assistant", content); 67 | const audio = await this.assistant.textToSpeech(content); 68 | if (selectedTool) { 69 | await this.call.pushAudio(audio); 70 | } else { 71 | this.call.pushAudio(audio); 72 | } 73 | } 74 | 75 | if (selectedTool) { 76 | this.addToCallLog("TOOL_SELECTED", { 77 | tool: selectedTool, 78 | }); 79 | } 80 | 81 | if (selectedTool === "endCall") { 82 | this.call.pushMeta("---- Assistant Hung Up ----"); 83 | this.call.end(); 84 | this.call.off("userMessage", this.startListening); 85 | return; 86 | } else if (selectedTool) { 87 | // TODO: implement custom tools 88 | console.warn( 89 | "[CUSTOM TOOLS NOT YET SUPPORTED] Unhandled tool:", 90 | selectedTool 91 | ); 92 | } 93 | }); 94 | } 95 | 96 | /** 97 | * Adds an event to the call log. 98 | * @param {string} event - Event to add to the call log. 99 | * @param {object} meta - Meta data to add to the call log. 100 | */ 101 | addToCallLog(event, meta) { 102 | const timestamp = new Date().toISOString(); 103 | this.callLog.push({ timestamp, event, meta }); 104 | } 105 | 106 | /** 107 | * Adds a message to the call log and history. 108 | * @param {string} who - Who said the message. 109 | * @param {string} message - Message to add to the call log and history. 110 | */ 111 | noteWhatWasSaid(speaker, message) { 112 | this.addToCallLog(`TRANSCRIPT`, { speaker, message }); 113 | this.history.push({ role: speaker, content: message }); 114 | this.call.pushMeta(`${speaker}: ${message}`); 115 | } 116 | } 117 | 118 | module.exports = { Conversation }; 119 | -------------------------------------------------------------------------------- /server/lib/platform/twilio.js: -------------------------------------------------------------------------------- 1 | const http = require("http"); 2 | const HttpDispatcher = require("httpdispatcher"); 3 | const WebSocketServer = require("websocket").server; 4 | 5 | class TwilioCallServer { 6 | constructor(host, onConnect) { 7 | this.host = host; 8 | this.onConnect = onConnect; 9 | } 10 | 11 | getTwiMLDocument(wssUrl) { 12 | return ` 13 | 14 | 15 | 16 | 17 | 18 | `; 19 | } 20 | 21 | serve(httpPort = 8080, twimlRoute = "/twiml", streamsRoute = "/stream") { 22 | const host = this.host; 23 | const dispatcher = new HttpDispatcher(); 24 | 25 | const handleRequest = (request, response) => { 26 | try { 27 | dispatcher.dispatch(request, response); 28 | } catch (err) { 29 | console.error(err); 30 | } 31 | } 32 | const wsserver = http.createServer(handleRequest); 33 | const mediaws = new WebSocketServer({ 34 | httpServer: wsserver, 35 | autoAcceptConnections: false, 36 | }); 37 | 38 | mediaws.on('request', (request) => { 39 | if (request.resourceURL.pathname === streamsRoute) { 40 | request.accept(); 41 | } else { 42 | request.reject(); 43 | console.log(`Connection rejected from ${request.origin}`); 44 | } 45 | }); 46 | 47 | mediaws.on("connect", (connection) => { 48 | console.log("Connected."); 49 | this.onConnect(connection); 50 | }); 51 | 52 | dispatcher.onPost(twimlRoute, (request, response) => { 53 | console.log("POST to TwiML"); 54 | response.writeHead(200, { "Content-Type": "text/xml" }); 55 | response.end(this.getTwiMLDocument(`wss://${this.host}${streamsRoute}`)); 56 | }); 57 | 58 | wsserver.listen(httpPort, () => { 59 | console.log(`Server listening on port ${httpPort} @ ${host}`); 60 | }); 61 | } 62 | } 63 | 64 | module.exports = { 65 | TwilioCallServer 66 | }; 67 | -------------------------------------------------------------------------------- /server/lib/stt.js: -------------------------------------------------------------------------------- 1 | const OpenAI = require("openai"); 2 | const { float32ToPCM16, float32_pcm16ToWavBlob } = require("./audio"); 3 | const { createClient : Deepgram, LiveTranscriptionEvents } = require("@deepgram/sdk"); 4 | const { clearInterval } = require("timers"); 5 | 6 | const openai = new OpenAI({ 7 | apiKey: process.env.OPENAI_API_KEY, 8 | }); 9 | 10 | const STT_MODELS = [ 11 | "openai/whisper-1", 12 | "deepgram/nova-2", 13 | "deepgram/whisper", 14 | "deepgram:live/nova-2", // Streaming WS 15 | ]; 16 | 17 | const STT_AUDIO_FORMATS = { 18 | PCM_24K: "pcm_24000", 19 | MULAW_8K: "mulaw_8000", 20 | } 21 | 22 | class SpeechToText { 23 | 24 | constructor(modelID, format=STT_AUDIO_FORMATS.PCM_24K) { 25 | const { provider, model } = this._parseModel(modelID ?? 'openai/whisper-1'); 26 | this.model = model; 27 | this.provider = provider; 28 | this.format = format; 29 | this.sttObject = null; 30 | this.stt = (() => { 31 | switch (this.provider) { 32 | case "deepgram": 33 | if (!process.env.DEEPGRAM_API_KEY) { 34 | throw new Error( 35 | "DEEPGRAM_API_KEY is required to use deepgram for SpeechToText" 36 | ); 37 | } 38 | return transcribeDeepgram; 39 | 40 | case "deepgram:live": 41 | if (!process.env.DEEPGRAM_API_KEY) { 42 | throw new Error( 43 | "DEEPGRAM_API_KEY is required to use deepgram for SpeechToText" 44 | ); 45 | } 46 | const transcriber = new BatchedDeepgramRealtimeTranscriber(model, format, {keepAlive: true, connectOnInit: true}); 47 | this.sttObject = transcriber; 48 | return transcriber.transcribe.bind(transcriber); 49 | case "openai": 50 | if (!process.env.OPENAI_API_KEY) { 51 | throw new Error( 52 | "OPENAI_API_KEY is required to use openai for SpeechToText" 53 | ); 54 | } 55 | return transcribeWhisper; 56 | } 57 | })(); 58 | 59 | if (!STT_MODELS.includes(modelID ?? 'openai/whisper-1')) { 60 | throw new Error(`Unsupported STT model: ${modelID}`); 61 | } 62 | } 63 | 64 | /** 65 | * Transcribe audio. 66 | * @param {ArrayBuffer} audio_sample - The audio sample 67 | * @returns {Promise} - The transcribed text 68 | */ 69 | async transcribe(audio_sample) { 70 | return this.stt(this.model, audio_sample, this.format); 71 | } 72 | 73 | async destroy() { 74 | this.sttObject?.destroy(); 75 | } 76 | 77 | _parseModel(model) { 78 | const parts = model.split("/"); 79 | return { 80 | provider: parts[0], 81 | model: parts[1], 82 | }; 83 | } 84 | } 85 | 86 | async function transcribeDeepgram(model, audio_sample, format) { 87 | const deepgram = Deepgram(process.env.DEEPGRAM_API_KEY); 88 | if (format === STT_AUDIO_FORMATS.PCM_24K) { 89 | audio_sample = float32_pcm16ToWavBlob(audio_sample); 90 | } 91 | else { 92 | // TODO: Need to convert to PCM_24K 93 | throw new Error(`Deepgram unsupported audio format: ${format}`); 94 | } 95 | const { result, error } = await deepgram.listen.prerecorded.transcribeFile( 96 | wavBlob, 97 | { 98 | model: model, 99 | smart_format: false, 100 | mode: false, 101 | } 102 | ); 103 | 104 | if (error) { 105 | console.error(error); 106 | return; 107 | } 108 | const transcript = result.results.channels[0].alternatives.reduce((acc, alt) => { 109 | return acc + alt.transcript; 110 | }, ""); 111 | return transcript; 112 | } 113 | 114 | async function transcribeWhisper(model, audio_sample, format) { 115 | if (format === STT_AUDIO_FORMATS.PCM_24K) { 116 | audio_sample = float32_pcm16ToWavBlob(audio_sample); 117 | } 118 | else { 119 | // TODO: Need to convert to PCM_24K 120 | throw new Error(`OpenAI unsupported audio format: ${format}`); 121 | } 122 | const transcription = await openai.audio.transcriptions.create({ 123 | model, 124 | file: audio_sample, 125 | }); 126 | return transcription.text; 127 | } 128 | 129 | // TODO: Refactor to stream up audio in realtime instead of giving it all in the .transcribe. 130 | class BatchedDeepgramRealtimeTranscriber { 131 | /** 132 | * @param {string} model - Deepgram model to use 133 | * @param {string} format - Audio format to use 134 | * @param {object} options - Options for the transcriber 135 | * @param {boolean} options.keepAlive - Whether to keep the connection alive 136 | * @param {number} options.waitTimeAfterFirstChunk - Time to wait for transcription to finish. Default 200ms 137 | * @param {boolean} options.connectOnInit - Connect in constructor? Defaults to false. 138 | */ 139 | constructor(model, format=STT_AUDIO_FORMATS.PCM_24K, options={}) { 140 | this.model = model; 141 | this.format = format; 142 | this.deepgram = Deepgram(process.env.DEEPGRAM_API_KEY); 143 | this.waitTimeAfterSend = options.waitTimeAfterFirstChunk ?? 1000; 144 | this._connection = null; 145 | this.connectionOpen = false; 146 | this.keepAlive = options.keepAlive ?? true; 147 | this.keepAliveInterval = setInterval(this.keepAliveLoop.bind(this), 5000); 148 | if (options.connectOnInit ?? false) { 149 | this.getConnection(); 150 | } 151 | } 152 | 153 | async transcribe(model, audio_sample, format) { 154 | const connection = await this.getConnection(); 155 | 156 | if (!this.connectionOpen) { 157 | throw new Error("Deepgram connection not open"); 158 | } 159 | 160 | if (format === STT_AUDIO_FORMATS.PCM_24K) { 161 | audio_sample = float32ToPCM16(audio_sample); 162 | } 163 | connection.send(audio_sample); 164 | 165 | const collected = []; 166 | let lastWasPending = false; 167 | 168 | const createResult = () => { 169 | return collected.join(" "); 170 | } 171 | 172 | return new Promise((resolve, reject) => { 173 | const timeoutId = setTimeout(() => { 174 | resolve(createResult()); 175 | }, this.waitTimeAfterSend); 176 | 177 | const handleTranscript = (data) => { 178 | 179 | if (data.is_final && lastWasPending) { 180 | collected[collected.length - 1] = data.channel.alternatives[0].transcript; 181 | } else { 182 | collected.push(data.channel.alternatives[0].transcript); 183 | } 184 | 185 | lastWasPending = data.is_final; 186 | 187 | if (data.speech_final) { 188 | clearTimeout(timeoutId); 189 | resolve(createResult()); 190 | connection.removeListener(LiveTranscriptionEvents.Transcript, handleTranscript); 191 | } 192 | }; 193 | 194 | connection.on(LiveTranscriptionEvents.Transcript, handleTranscript); 195 | }); 196 | } 197 | 198 | async getConnection() { 199 | if (this._connection) { 200 | return this._connection; 201 | } 202 | 203 | let promiseResolve, promiseReject; 204 | this._connection = new Promise((resolve, reject) => { 205 | promiseResolve = resolve; 206 | promiseReject = reject; 207 | }); 208 | 209 | const connection = this.deepgram.listen.live({ 210 | model: this.model, 211 | smart_format: false, 212 | interim_results: true, 213 | numerals: true, 214 | endpointing: 200, 215 | vad_events: true, 216 | interim_results: true, 217 | utterance_end_ms: "1000", 218 | encoding: this.format === STT_AUDIO_FORMATS.PCM_24K ? "linear16" : "mulaw", 219 | sample_rate: this.format === STT_AUDIO_FORMATS.PCM_24K ? 24000 : 8000, 220 | }); 221 | 222 | try { 223 | const setupTimeout = setTimeout(() => promiseReject("Failed to setup deepgram connection"), 5000); 224 | connection.on(LiveTranscriptionEvents.Open, () => { 225 | this.connectionOpen = true; 226 | console.log("Deepgram Live Connection opened."); 227 | clearTimeout(setupTimeout); 228 | promiseResolve(connection); 229 | 230 | connection.on(LiveTranscriptionEvents.Transcript, (data) => { 231 | if (!data.is_final) { 232 | console.log("Interim result:", data.channel.alternatives[0].transcript); 233 | return; 234 | } 235 | 236 | const transcript = data.channel.alternatives.reduce((acc, alt) => { 237 | return acc + alt.transcript; 238 | }, ""); 239 | console.log("Final result:", transcript); 240 | 241 | if (data.speech_final) { 242 | console.log("Speech final event received"); 243 | } 244 | 245 | }); 246 | 247 | connection.on(LiveTranscriptionEvents.Close, () => { 248 | this.connectionOpen = false; 249 | clearInterval(this.keepAliveInterval); 250 | console.log("Deepgram Live Connection closed."); 251 | }) 252 | 253 | connection.on(LiveTranscriptionEvents.Error, (err) => { 254 | console.error(err); 255 | }) 256 | }); 257 | } catch (err) { 258 | promiseReject(err); 259 | } 260 | 261 | return this._connection; 262 | } 263 | 264 | async keepAliveLoop() { 265 | const connection = await this._connection; 266 | if (connection && this.connectionOpen && connection.keepAlive) { 267 | connection.keepAlive(); 268 | } 269 | else { 270 | clearInterval(this.keepAliveInterval); 271 | } 272 | } 273 | 274 | async destroy () { 275 | if (this.connectionOpen) { 276 | (await this._connection).finish(); 277 | } 278 | } 279 | 280 | } 281 | 282 | module.exports = { 283 | SpeechToText, 284 | transcribeWhisper, 285 | transcribeDeepgram, 286 | }; 287 | -------------------------------------------------------------------------------- /server/lib/tts.js: -------------------------------------------------------------------------------- 1 | const fetch = require("node-fetch"); 2 | const OpenAI = require("openai"); 3 | const util = require("util"); 4 | const PlayHT = require("playht"); 5 | const { createClient: Deepgram } = require("@deepgram/sdk"); 6 | 7 | const openai = new OpenAI({ 8 | apiKey: process.env.OPENAI_API_KEY, 9 | }); 10 | 11 | const TTS_MODELS = [ 12 | "elevenlabs/eleven_monolingual_v1", 13 | "elevenlabs/eleven_turbo_v2", 14 | "elevenlabs/eleven_multilingual_v2", 15 | "openai/tts-1", 16 | "openai/tts-1-hd", 17 | "playht/PlayHT2.0-turbo", 18 | "playht/PlayHT2.0", 19 | "playht/PlayHT1.0", 20 | "deepgram/aura", 21 | ]; 22 | 23 | const TTS_AUDIO_FORMATS = { 24 | PCM_24K: "pcm_24000", 25 | MULAW_8K: "mulaw_8000", 26 | }; 27 | 28 | class TextToSpeech { 29 | constructor(modelID, voice, format = TTS_AUDIO_FORMATS.PCM_24K) { 30 | if (!TTS_MODELS.includes(modelID)) { 31 | throw new Error(`Unsupported TTS model: ${modelID}`); 32 | } 33 | const { provider, model } = this._parseModel(modelID); 34 | this.tts = (() => { 35 | switch (provider) { 36 | case "elevenlabs": 37 | return tts_elevenlabs; 38 | case "openai": 39 | return tts_openai; 40 | case "playht": 41 | return tts_playhts; 42 | case "deepgram": 43 | return tts_deepgram; 44 | } 45 | })(); 46 | this.model = model; 47 | this.voice = voice; 48 | this.format = format; 49 | 50 | // Do check for creds 51 | if (provider === "elevenlabs") { 52 | if (!process.env.ELEVEN_LABS_API_KEY) { 53 | throw new Error( 54 | "ELEVEN_LABS_API_KEY is required to use elevenlabs for TextToSpeech" 55 | ); 56 | } 57 | } 58 | 59 | if (provider === "openai") { 60 | if (!process.env.OPENAI_API_KEY) { 61 | throw new Error( 62 | "OPENAI_API_KEY is required to use openai for TextToSpeech" 63 | ); 64 | } 65 | } 66 | 67 | if (provider === "deepgram") { 68 | if (!process.env.DEEPGRAM_API_KEY) { 69 | throw new Error( 70 | "DEEPGRAM_API_KEY is required to use deepgram for TextToSpeech" 71 | ); 72 | } 73 | } 74 | 75 | if (provider === "playht") { 76 | if (!process.env.PLAYHT_USER_ID || !process.env.PLAYHT_API_KEY) { 77 | throw new Error( 78 | "PLAYHT_USER_ID and PLAYHT_API_KEY are required to use playht for TextToSpeech" 79 | ); 80 | } 81 | } 82 | } 83 | 84 | /** 85 | * Synthesize a message into audio playable over the wire by our client. 86 | * @param {string} message - The message to synthesize 87 | * @returns {Buffer | ArrayBuffer} - The audio data - PCM. 24k sample rate, 16 bit depth, 1 channel 88 | */ 89 | async synthesize(message) { 90 | return this.tts(message, this.model, this.voice, this.format); 91 | } 92 | 93 | _parseModel(model) { 94 | const parts = model.split("/"); 95 | return { 96 | provider: parts[0], 97 | model: parts[1], 98 | }; 99 | } 100 | } 101 | 102 | async function tts_elevenlabs(message, model, voice, format) { 103 | const body = { 104 | text: message, 105 | model, 106 | voice_settings: { 107 | stability: 0.5, 108 | similarity_boost: 0.8, 109 | style: 0, 110 | }, 111 | }; 112 | 113 | const query = { 114 | output_format: "pcm_24000", 115 | }; 116 | 117 | if (format === TTS_AUDIO_FORMATS.MULAW_8K) { 118 | query.output_format = "ulaw_8000"; 119 | } 120 | 121 | const options = { 122 | method: "POST", 123 | headers: { 124 | "xi-api-key": process.env.ELEVEN_LABS_API_KEY, 125 | "Content-Type": "application/json", 126 | }, 127 | body: JSON.stringify(body), 128 | }; 129 | 130 | const response = await fetch( 131 | `https://api.elevenlabs.io/v1/text-to-speech/${voice}?${new URLSearchParams( 132 | query 133 | )}`, 134 | options 135 | ); 136 | 137 | if (response.status !== 200) { 138 | console.error( 139 | "Failed to generate audio", 140 | response.status, 141 | response.statusText, 142 | util.inspect(await response.json(), { depth: null, colors: true }) 143 | ); 144 | return; 145 | } 146 | const arrayBuffer = await response.arrayBuffer(); 147 | const audio = new Int16Array(arrayBuffer); 148 | return audio; 149 | } 150 | 151 | async function tts_openai(message, model, voice, format) { 152 | const response = await openai.audio.speech.create({ 153 | model, 154 | voice, 155 | input: message, 156 | response_format: "pcm", 157 | }); 158 | 159 | if (format !== TTS_AUDIO_FORMATS.PCM_24K) { 160 | throw new Error(`TODO: OpenAI unsupported audio format: ${format}`); 161 | } 162 | 163 | const arrayBuffer = await response.arrayBuffer(); 164 | return arrayBuffer; 165 | } 166 | 167 | async function tts_playhts(message, model, voice, format) { 168 | PlayHT.init({ 169 | userId: process.env.PLAYHT_USER_ID, 170 | apiKey: process.env.PLAYHT_API_KEY, 171 | }); 172 | 173 | const streamingOptions = { 174 | voiceEngine: model, 175 | voiceId: voice, 176 | speed: 1, 177 | ...(format === TTS_AUDIO_FORMATS.MULAW_8K 178 | ? { 179 | outputFormat: "mulaw", 180 | sampleRate: 8000, 181 | } 182 | : { 183 | // This is a hack because im not sure why "raw" sounds so weird. 184 | // With wav, we take off the first 44 bytes that make up the wav header 185 | // and it's effectively pcm. 186 | outputFormat: "wav", 187 | sampleRate: 24000, 188 | }), 189 | }; 190 | try { 191 | const stream = await PlayHT.stream(message, streamingOptions); 192 | return await new Promise((resolve, reject) => { 193 | const chunks = []; 194 | let totalSize = 0; 195 | 196 | stream.on("data", (chunk) => { 197 | chunks.push(chunk); 198 | totalSize += chunk.length; 199 | }); 200 | 201 | stream.on("end", () => { 202 | const final = Buffer.alloc(totalSize); 203 | let offset = 0; 204 | for (const chunk of chunks) { 205 | final.set(chunk, offset); 206 | offset += chunk.length; 207 | } 208 | if (format === TTS_AUDIO_FORMATS.PCM_24K) { 209 | // slice off first 44 bytes for wav header 210 | const audio = final.slice(44); 211 | resolve(new Int16Array(audio.buffer)); 212 | } else { 213 | resolve(final); 214 | } 215 | 216 | }); 217 | 218 | stream.on("error", (err) => { 219 | reject(err); 220 | }); 221 | }); 222 | } catch (err) { 223 | console.error(err); 224 | return; 225 | } 226 | } 227 | 228 | // NOTE: Deepgram usese the format {model}-{voice}-{language} (i.e. aura-luna-en). 229 | // User should provide luna-en as the voice to use. 230 | const tts_deepgram = async (message, model, voice, format) => { 231 | const deepgram = Deepgram(process.env.DEEPGRAM_API_KEY); 232 | 233 | const deepgramModel = model + "-" + voice; 234 | // NOTE: voice not applicable 235 | const response = await deepgram.speak.request( 236 | { text: message }, 237 | { 238 | model: deepgramModel, 239 | encoding: format === TTS_AUDIO_FORMATS.PCM_24K ? "linear16" : "mulaw", 240 | sample_rate: format === TTS_AUDIO_FORMATS.PCM_24K ? 24000 : 8000, 241 | container: "none", 242 | } 243 | ); 244 | const stream = await response.getStream(); 245 | return new Promise(async (resolve, reject) => { 246 | try { 247 | const chunks = []; 248 | for await (const chunk of stream.values()) { 249 | chunks.push(chunk); 250 | } 251 | if (format === TTS_AUDIO_FORMATS.PCM_24K) { 252 | resolve(new Int16Array(Buffer.concat(chunks).buffer)); 253 | } else { 254 | resolve(Buffer.concat(chunks)); 255 | } 256 | } catch (err) { 257 | reject(err); 258 | } 259 | }); 260 | }; 261 | 262 | module.exports = { 263 | TextToSpeech, 264 | tts_elevenlabs, 265 | tts_openai, 266 | tts_playhts, 267 | tts_deepgram, 268 | TTS_MODELS, 269 | TTS_AUDIO_FORMATS, 270 | }; 271 | -------------------------------------------------------------------------------- /server/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "server", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@deepgram/sdk": "^3.3.5", 13 | "dotenv": "^16.4.5", 14 | "express": "^4.19.2", 15 | "node-fetch": "^2.7.0", 16 | "openai": "^4.52.0", 17 | "playht": "^0.9.8", 18 | "ws": "^8.17.1" 19 | }, 20 | "devDependencies": { 21 | "nodemon": "^3.1.3" 22 | } 23 | }, 24 | "node_modules/@deepgram/captions": { 25 | "version": "1.2.0", 26 | "resolved": "https://registry.npmjs.org/@deepgram/captions/-/captions-1.2.0.tgz", 27 | "integrity": "sha512-8B1C/oTxTxyHlSFubAhNRgCbQ2SQ5wwvtlByn8sDYZvdDtdn/VE2yEPZ4BvUnrKWmsbTQY6/ooLV+9Ka2qmDSQ==", 28 | "dependencies": { 29 | "dayjs": "^1.11.10" 30 | }, 31 | "engines": { 32 | "node": ">=18.0.0" 33 | } 34 | }, 35 | "node_modules/@deepgram/sdk": { 36 | "version": "3.3.5", 37 | "resolved": "https://registry.npmjs.org/@deepgram/sdk/-/sdk-3.3.5.tgz", 38 | "integrity": "sha512-CZ3GLZUnzCUdOS2CEDnC4+j7WAyrpUPBKkEKplKFt3sK6xkpk2fuhCARVxNJ18VhCRkcnhv2uSBXDjiuP5J1dA==", 39 | "dependencies": { 40 | "@deepgram/captions": "^1.1.1", 41 | "@types/websocket": "^1.0.9", 42 | "cross-fetch": "^3.1.5", 43 | "deepmerge": "^4.3.1", 44 | "events": "^3.3.0", 45 | "websocket": "^1.0.34" 46 | }, 47 | "engines": { 48 | "node": ">=18.0.0" 49 | } 50 | }, 51 | "node_modules/@deepgram/sdk/node_modules/cross-fetch": { 52 | "version": "3.1.8", 53 | "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", 54 | "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", 55 | "dependencies": { 56 | "node-fetch": "^2.6.12" 57 | } 58 | }, 59 | "node_modules/@grpc/grpc-js": { 60 | "version": "1.10.10", 61 | "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.10.tgz", 62 | "integrity": "sha512-HPa/K5NX6ahMoeBv15njAc/sfF4/jmiXLar9UlC2UfHFKZzsCVLc3wbe7+7qua7w9VPh2/L6EBxyAV7/E8Wftg==", 63 | "dependencies": { 64 | "@grpc/proto-loader": "^0.7.13", 65 | "@js-sdsl/ordered-map": "^4.4.2" 66 | }, 67 | "engines": { 68 | "node": ">=12.10.0" 69 | } 70 | }, 71 | "node_modules/@grpc/proto-loader": { 72 | "version": "0.7.13", 73 | "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", 74 | "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", 75 | "dependencies": { 76 | "lodash.camelcase": "^4.3.0", 77 | "long": "^5.0.0", 78 | "protobufjs": "^7.2.5", 79 | "yargs": "^17.7.2" 80 | }, 81 | "bin": { 82 | "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" 83 | }, 84 | "engines": { 85 | "node": ">=6" 86 | } 87 | }, 88 | "node_modules/@js-sdsl/ordered-map": { 89 | "version": "4.4.2", 90 | "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", 91 | "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", 92 | "funding": { 93 | "type": "opencollective", 94 | "url": "https://opencollective.com/js-sdsl" 95 | } 96 | }, 97 | "node_modules/@protobufjs/aspromise": { 98 | "version": "1.1.2", 99 | "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", 100 | "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" 101 | }, 102 | "node_modules/@protobufjs/base64": { 103 | "version": "1.1.2", 104 | "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", 105 | "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" 106 | }, 107 | "node_modules/@protobufjs/codegen": { 108 | "version": "2.0.4", 109 | "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", 110 | "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" 111 | }, 112 | "node_modules/@protobufjs/eventemitter": { 113 | "version": "1.1.0", 114 | "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", 115 | "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" 116 | }, 117 | "node_modules/@protobufjs/fetch": { 118 | "version": "1.1.0", 119 | "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", 120 | "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", 121 | "dependencies": { 122 | "@protobufjs/aspromise": "^1.1.1", 123 | "@protobufjs/inquire": "^1.1.0" 124 | } 125 | }, 126 | "node_modules/@protobufjs/float": { 127 | "version": "1.0.2", 128 | "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", 129 | "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" 130 | }, 131 | "node_modules/@protobufjs/inquire": { 132 | "version": "1.1.0", 133 | "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", 134 | "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" 135 | }, 136 | "node_modules/@protobufjs/path": { 137 | "version": "1.1.2", 138 | "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", 139 | "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" 140 | }, 141 | "node_modules/@protobufjs/pool": { 142 | "version": "1.1.0", 143 | "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", 144 | "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" 145 | }, 146 | "node_modules/@protobufjs/utf8": { 147 | "version": "1.1.0", 148 | "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", 149 | "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" 150 | }, 151 | "node_modules/@tokenizer/token": { 152 | "version": "0.3.0", 153 | "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", 154 | "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" 155 | }, 156 | "node_modules/@types/node": { 157 | "version": "18.19.38", 158 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.38.tgz", 159 | "integrity": "sha512-SApYXUF7si4JJ+lO2o6X60OPOnA6wPpbiB09GMCkQ+JAwpa9hxUVG8p7GzA08TKQn5OhzK57rj1wFj+185YsGg==", 160 | "dependencies": { 161 | "undici-types": "~5.26.4" 162 | } 163 | }, 164 | "node_modules/@types/node-fetch": { 165 | "version": "2.6.11", 166 | "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", 167 | "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", 168 | "dependencies": { 169 | "@types/node": "*", 170 | "form-data": "^4.0.0" 171 | } 172 | }, 173 | "node_modules/@types/websocket": { 174 | "version": "1.0.10", 175 | "resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.10.tgz", 176 | "integrity": "sha512-svjGZvPB7EzuYS94cI7a+qhwgGU1y89wUgjT6E2wVUfmAGIvRfT7obBvRtnhXCSsoMdlG4gBFGE7MfkIXZLoww==", 177 | "dependencies": { 178 | "@types/node": "*" 179 | } 180 | }, 181 | "node_modules/abort-controller": { 182 | "version": "3.0.0", 183 | "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", 184 | "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", 185 | "dependencies": { 186 | "event-target-shim": "^5.0.0" 187 | }, 188 | "engines": { 189 | "node": ">=6.5" 190 | } 191 | }, 192 | "node_modules/accepts": { 193 | "version": "1.3.8", 194 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 195 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 196 | "dependencies": { 197 | "mime-types": "~2.1.34", 198 | "negotiator": "0.6.3" 199 | }, 200 | "engines": { 201 | "node": ">= 0.6" 202 | } 203 | }, 204 | "node_modules/agentkeepalive": { 205 | "version": "4.5.0", 206 | "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", 207 | "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", 208 | "dependencies": { 209 | "humanize-ms": "^1.2.1" 210 | }, 211 | "engines": { 212 | "node": ">= 8.0.0" 213 | } 214 | }, 215 | "node_modules/ansi-regex": { 216 | "version": "5.0.1", 217 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 218 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 219 | "engines": { 220 | "node": ">=8" 221 | } 222 | }, 223 | "node_modules/ansi-styles": { 224 | "version": "4.3.0", 225 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 226 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 227 | "dependencies": { 228 | "color-convert": "^2.0.1" 229 | }, 230 | "engines": { 231 | "node": ">=8" 232 | }, 233 | "funding": { 234 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 235 | } 236 | }, 237 | "node_modules/anymatch": { 238 | "version": "3.1.3", 239 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 240 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 241 | "dev": true, 242 | "dependencies": { 243 | "normalize-path": "^3.0.0", 244 | "picomatch": "^2.0.4" 245 | }, 246 | "engines": { 247 | "node": ">= 8" 248 | } 249 | }, 250 | "node_modules/array-flatten": { 251 | "version": "1.1.1", 252 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 253 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" 254 | }, 255 | "node_modules/asynckit": { 256 | "version": "0.4.0", 257 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 258 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 259 | }, 260 | "node_modules/axios": { 261 | "version": "1.7.2", 262 | "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", 263 | "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", 264 | "dependencies": { 265 | "follow-redirects": "^1.15.6", 266 | "form-data": "^4.0.0", 267 | "proxy-from-env": "^1.1.0" 268 | } 269 | }, 270 | "node_modules/balanced-match": { 271 | "version": "1.0.2", 272 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 273 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 274 | "dev": true 275 | }, 276 | "node_modules/binary-extensions": { 277 | "version": "2.3.0", 278 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", 279 | "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", 280 | "dev": true, 281 | "engines": { 282 | "node": ">=8" 283 | }, 284 | "funding": { 285 | "url": "https://github.com/sponsors/sindresorhus" 286 | } 287 | }, 288 | "node_modules/body-parser": { 289 | "version": "1.20.2", 290 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", 291 | "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", 292 | "dependencies": { 293 | "bytes": "3.1.2", 294 | "content-type": "~1.0.5", 295 | "debug": "2.6.9", 296 | "depd": "2.0.0", 297 | "destroy": "1.2.0", 298 | "http-errors": "2.0.0", 299 | "iconv-lite": "0.4.24", 300 | "on-finished": "2.4.1", 301 | "qs": "6.11.0", 302 | "raw-body": "2.5.2", 303 | "type-is": "~1.6.18", 304 | "unpipe": "1.0.0" 305 | }, 306 | "engines": { 307 | "node": ">= 0.8", 308 | "npm": "1.2.8000 || >= 1.4.16" 309 | } 310 | }, 311 | "node_modules/brace-expansion": { 312 | "version": "1.1.11", 313 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 314 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 315 | "dev": true, 316 | "dependencies": { 317 | "balanced-match": "^1.0.0", 318 | "concat-map": "0.0.1" 319 | } 320 | }, 321 | "node_modules/braces": { 322 | "version": "3.0.3", 323 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", 324 | "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", 325 | "dev": true, 326 | "dependencies": { 327 | "fill-range": "^7.1.1" 328 | }, 329 | "engines": { 330 | "node": ">=8" 331 | } 332 | }, 333 | "node_modules/bufferutil": { 334 | "version": "4.0.8", 335 | "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", 336 | "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", 337 | "hasInstallScript": true, 338 | "dependencies": { 339 | "node-gyp-build": "^4.3.0" 340 | }, 341 | "engines": { 342 | "node": ">=6.14.2" 343 | } 344 | }, 345 | "node_modules/bytes": { 346 | "version": "3.1.2", 347 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 348 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 349 | "engines": { 350 | "node": ">= 0.8" 351 | } 352 | }, 353 | "node_modules/call-bind": { 354 | "version": "1.0.7", 355 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", 356 | "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", 357 | "dependencies": { 358 | "es-define-property": "^1.0.0", 359 | "es-errors": "^1.3.0", 360 | "function-bind": "^1.1.2", 361 | "get-intrinsic": "^1.2.4", 362 | "set-function-length": "^1.2.1" 363 | }, 364 | "engines": { 365 | "node": ">= 0.4" 366 | }, 367 | "funding": { 368 | "url": "https://github.com/sponsors/ljharb" 369 | } 370 | }, 371 | "node_modules/chokidar": { 372 | "version": "3.6.0", 373 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", 374 | "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", 375 | "dev": true, 376 | "dependencies": { 377 | "anymatch": "~3.1.2", 378 | "braces": "~3.0.2", 379 | "glob-parent": "~5.1.2", 380 | "is-binary-path": "~2.1.0", 381 | "is-glob": "~4.0.1", 382 | "normalize-path": "~3.0.0", 383 | "readdirp": "~3.6.0" 384 | }, 385 | "engines": { 386 | "node": ">= 8.10.0" 387 | }, 388 | "funding": { 389 | "url": "https://paulmillr.com/funding/" 390 | }, 391 | "optionalDependencies": { 392 | "fsevents": "~2.3.2" 393 | } 394 | }, 395 | "node_modules/cliui": { 396 | "version": "8.0.1", 397 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", 398 | "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", 399 | "dependencies": { 400 | "string-width": "^4.2.0", 401 | "strip-ansi": "^6.0.1", 402 | "wrap-ansi": "^7.0.0" 403 | }, 404 | "engines": { 405 | "node": ">=12" 406 | } 407 | }, 408 | "node_modules/color-convert": { 409 | "version": "2.0.1", 410 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 411 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 412 | "dependencies": { 413 | "color-name": "~1.1.4" 414 | }, 415 | "engines": { 416 | "node": ">=7.0.0" 417 | } 418 | }, 419 | "node_modules/color-name": { 420 | "version": "1.1.4", 421 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 422 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 423 | }, 424 | "node_modules/combined-stream": { 425 | "version": "1.0.8", 426 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 427 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 428 | "dependencies": { 429 | "delayed-stream": "~1.0.0" 430 | }, 431 | "engines": { 432 | "node": ">= 0.8" 433 | } 434 | }, 435 | "node_modules/concat-map": { 436 | "version": "0.0.1", 437 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 438 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 439 | "dev": true 440 | }, 441 | "node_modules/content-disposition": { 442 | "version": "0.5.4", 443 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 444 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 445 | "dependencies": { 446 | "safe-buffer": "5.2.1" 447 | }, 448 | "engines": { 449 | "node": ">= 0.6" 450 | } 451 | }, 452 | "node_modules/content-type": { 453 | "version": "1.0.5", 454 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 455 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 456 | "engines": { 457 | "node": ">= 0.6" 458 | } 459 | }, 460 | "node_modules/cookie": { 461 | "version": "0.6.0", 462 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", 463 | "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", 464 | "engines": { 465 | "node": ">= 0.6" 466 | } 467 | }, 468 | "node_modules/cookie-signature": { 469 | "version": "1.0.6", 470 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 471 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" 472 | }, 473 | "node_modules/cross-fetch": { 474 | "version": "4.0.0", 475 | "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", 476 | "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", 477 | "dependencies": { 478 | "node-fetch": "^2.6.12" 479 | } 480 | }, 481 | "node_modules/d": { 482 | "version": "1.0.2", 483 | "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", 484 | "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", 485 | "dependencies": { 486 | "es5-ext": "^0.10.64", 487 | "type": "^2.7.2" 488 | }, 489 | "engines": { 490 | "node": ">=0.12" 491 | } 492 | }, 493 | "node_modules/dayjs": { 494 | "version": "1.11.11", 495 | "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz", 496 | "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==" 497 | }, 498 | "node_modules/debug": { 499 | "version": "2.6.9", 500 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 501 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 502 | "dependencies": { 503 | "ms": "2.0.0" 504 | } 505 | }, 506 | "node_modules/deepmerge": { 507 | "version": "4.3.1", 508 | "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", 509 | "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", 510 | "engines": { 511 | "node": ">=0.10.0" 512 | } 513 | }, 514 | "node_modules/define-data-property": { 515 | "version": "1.1.4", 516 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", 517 | "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", 518 | "dependencies": { 519 | "es-define-property": "^1.0.0", 520 | "es-errors": "^1.3.0", 521 | "gopd": "^1.0.1" 522 | }, 523 | "engines": { 524 | "node": ">= 0.4" 525 | }, 526 | "funding": { 527 | "url": "https://github.com/sponsors/ljharb" 528 | } 529 | }, 530 | "node_modules/delayed-stream": { 531 | "version": "1.0.0", 532 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 533 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 534 | "engines": { 535 | "node": ">=0.4.0" 536 | } 537 | }, 538 | "node_modules/depd": { 539 | "version": "2.0.0", 540 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 541 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 542 | "engines": { 543 | "node": ">= 0.8" 544 | } 545 | }, 546 | "node_modules/destroy": { 547 | "version": "1.2.0", 548 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 549 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 550 | "engines": { 551 | "node": ">= 0.8", 552 | "npm": "1.2.8000 || >= 1.4.16" 553 | } 554 | }, 555 | "node_modules/dotenv": { 556 | "version": "16.4.5", 557 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", 558 | "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", 559 | "engines": { 560 | "node": ">=12" 561 | }, 562 | "funding": { 563 | "url": "https://dotenvx.com" 564 | } 565 | }, 566 | "node_modules/ee-first": { 567 | "version": "1.1.1", 568 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 569 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 570 | }, 571 | "node_modules/emoji-regex": { 572 | "version": "8.0.0", 573 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 574 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 575 | }, 576 | "node_modules/encodeurl": { 577 | "version": "1.0.2", 578 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 579 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 580 | "engines": { 581 | "node": ">= 0.8" 582 | } 583 | }, 584 | "node_modules/es-define-property": { 585 | "version": "1.0.0", 586 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", 587 | "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", 588 | "dependencies": { 589 | "get-intrinsic": "^1.2.4" 590 | }, 591 | "engines": { 592 | "node": ">= 0.4" 593 | } 594 | }, 595 | "node_modules/es-errors": { 596 | "version": "1.3.0", 597 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 598 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 599 | "engines": { 600 | "node": ">= 0.4" 601 | } 602 | }, 603 | "node_modules/es5-ext": { 604 | "version": "0.10.64", 605 | "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", 606 | "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", 607 | "hasInstallScript": true, 608 | "dependencies": { 609 | "es6-iterator": "^2.0.3", 610 | "es6-symbol": "^3.1.3", 611 | "esniff": "^2.0.1", 612 | "next-tick": "^1.1.0" 613 | }, 614 | "engines": { 615 | "node": ">=0.10" 616 | } 617 | }, 618 | "node_modules/es6-iterator": { 619 | "version": "2.0.3", 620 | "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", 621 | "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", 622 | "dependencies": { 623 | "d": "1", 624 | "es5-ext": "^0.10.35", 625 | "es6-symbol": "^3.1.1" 626 | } 627 | }, 628 | "node_modules/es6-symbol": { 629 | "version": "3.1.4", 630 | "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", 631 | "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", 632 | "dependencies": { 633 | "d": "^1.0.2", 634 | "ext": "^1.7.0" 635 | }, 636 | "engines": { 637 | "node": ">=0.12" 638 | } 639 | }, 640 | "node_modules/escalade": { 641 | "version": "3.1.2", 642 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", 643 | "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", 644 | "engines": { 645 | "node": ">=6" 646 | } 647 | }, 648 | "node_modules/escape-html": { 649 | "version": "1.0.3", 650 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 651 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 652 | }, 653 | "node_modules/esniff": { 654 | "version": "2.0.1", 655 | "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", 656 | "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", 657 | "dependencies": { 658 | "d": "^1.0.1", 659 | "es5-ext": "^0.10.62", 660 | "event-emitter": "^0.3.5", 661 | "type": "^2.7.2" 662 | }, 663 | "engines": { 664 | "node": ">=0.10" 665 | } 666 | }, 667 | "node_modules/etag": { 668 | "version": "1.8.1", 669 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 670 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 671 | "engines": { 672 | "node": ">= 0.6" 673 | } 674 | }, 675 | "node_modules/event-emitter": { 676 | "version": "0.3.5", 677 | "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", 678 | "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", 679 | "dependencies": { 680 | "d": "1", 681 | "es5-ext": "~0.10.14" 682 | } 683 | }, 684 | "node_modules/event-target-shim": { 685 | "version": "5.0.1", 686 | "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", 687 | "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", 688 | "engines": { 689 | "node": ">=6" 690 | } 691 | }, 692 | "node_modules/events": { 693 | "version": "3.3.0", 694 | "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", 695 | "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", 696 | "engines": { 697 | "node": ">=0.8.x" 698 | } 699 | }, 700 | "node_modules/express": { 701 | "version": "4.19.2", 702 | "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", 703 | "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", 704 | "dependencies": { 705 | "accepts": "~1.3.8", 706 | "array-flatten": "1.1.1", 707 | "body-parser": "1.20.2", 708 | "content-disposition": "0.5.4", 709 | "content-type": "~1.0.4", 710 | "cookie": "0.6.0", 711 | "cookie-signature": "1.0.6", 712 | "debug": "2.6.9", 713 | "depd": "2.0.0", 714 | "encodeurl": "~1.0.2", 715 | "escape-html": "~1.0.3", 716 | "etag": "~1.8.1", 717 | "finalhandler": "1.2.0", 718 | "fresh": "0.5.2", 719 | "http-errors": "2.0.0", 720 | "merge-descriptors": "1.0.1", 721 | "methods": "~1.1.2", 722 | "on-finished": "2.4.1", 723 | "parseurl": "~1.3.3", 724 | "path-to-regexp": "0.1.7", 725 | "proxy-addr": "~2.0.7", 726 | "qs": "6.11.0", 727 | "range-parser": "~1.2.1", 728 | "safe-buffer": "5.2.1", 729 | "send": "0.18.0", 730 | "serve-static": "1.15.0", 731 | "setprototypeof": "1.2.0", 732 | "statuses": "2.0.1", 733 | "type-is": "~1.6.18", 734 | "utils-merge": "1.0.1", 735 | "vary": "~1.1.2" 736 | }, 737 | "engines": { 738 | "node": ">= 0.10.0" 739 | } 740 | }, 741 | "node_modules/ext": { 742 | "version": "1.7.0", 743 | "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", 744 | "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", 745 | "dependencies": { 746 | "type": "^2.7.2" 747 | } 748 | }, 749 | "node_modules/file-type": { 750 | "version": "18.7.0", 751 | "resolved": "https://registry.npmjs.org/file-type/-/file-type-18.7.0.tgz", 752 | "integrity": "sha512-ihHtXRzXEziMrQ56VSgU7wkxh55iNchFkosu7Y9/S+tXHdKyrGjVK0ujbqNnsxzea+78MaLhN6PGmfYSAv1ACw==", 753 | "dependencies": { 754 | "readable-web-to-node-stream": "^3.0.2", 755 | "strtok3": "^7.0.0", 756 | "token-types": "^5.0.1" 757 | }, 758 | "engines": { 759 | "node": ">=14.16" 760 | }, 761 | "funding": { 762 | "url": "https://github.com/sindresorhus/file-type?sponsor=1" 763 | } 764 | }, 765 | "node_modules/fill-range": { 766 | "version": "7.1.1", 767 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", 768 | "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", 769 | "dev": true, 770 | "dependencies": { 771 | "to-regex-range": "^5.0.1" 772 | }, 773 | "engines": { 774 | "node": ">=8" 775 | } 776 | }, 777 | "node_modules/finalhandler": { 778 | "version": "1.2.0", 779 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", 780 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", 781 | "dependencies": { 782 | "debug": "2.6.9", 783 | "encodeurl": "~1.0.2", 784 | "escape-html": "~1.0.3", 785 | "on-finished": "2.4.1", 786 | "parseurl": "~1.3.3", 787 | "statuses": "2.0.1", 788 | "unpipe": "~1.0.0" 789 | }, 790 | "engines": { 791 | "node": ">= 0.8" 792 | } 793 | }, 794 | "node_modules/follow-redirects": { 795 | "version": "1.15.6", 796 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", 797 | "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", 798 | "funding": [ 799 | { 800 | "type": "individual", 801 | "url": "https://github.com/sponsors/RubenVerborgh" 802 | } 803 | ], 804 | "engines": { 805 | "node": ">=4.0" 806 | }, 807 | "peerDependenciesMeta": { 808 | "debug": { 809 | "optional": true 810 | } 811 | } 812 | }, 813 | "node_modules/form-data": { 814 | "version": "4.0.0", 815 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 816 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 817 | "dependencies": { 818 | "asynckit": "^0.4.0", 819 | "combined-stream": "^1.0.8", 820 | "mime-types": "^2.1.12" 821 | }, 822 | "engines": { 823 | "node": ">= 6" 824 | } 825 | }, 826 | "node_modules/form-data-encoder": { 827 | "version": "1.7.2", 828 | "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", 829 | "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" 830 | }, 831 | "node_modules/formdata-node": { 832 | "version": "4.4.1", 833 | "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", 834 | "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", 835 | "dependencies": { 836 | "node-domexception": "1.0.0", 837 | "web-streams-polyfill": "4.0.0-beta.3" 838 | }, 839 | "engines": { 840 | "node": ">= 12.20" 841 | } 842 | }, 843 | "node_modules/formdata-node/node_modules/web-streams-polyfill": { 844 | "version": "4.0.0-beta.3", 845 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", 846 | "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", 847 | "engines": { 848 | "node": ">= 14" 849 | } 850 | }, 851 | "node_modules/forwarded": { 852 | "version": "0.2.0", 853 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 854 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 855 | "engines": { 856 | "node": ">= 0.6" 857 | } 858 | }, 859 | "node_modules/fresh": { 860 | "version": "0.5.2", 861 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 862 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 863 | "engines": { 864 | "node": ">= 0.6" 865 | } 866 | }, 867 | "node_modules/fsevents": { 868 | "version": "2.3.3", 869 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 870 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 871 | "dev": true, 872 | "hasInstallScript": true, 873 | "optional": true, 874 | "os": [ 875 | "darwin" 876 | ], 877 | "engines": { 878 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 879 | } 880 | }, 881 | "node_modules/function-bind": { 882 | "version": "1.1.2", 883 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 884 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 885 | "funding": { 886 | "url": "https://github.com/sponsors/ljharb" 887 | } 888 | }, 889 | "node_modules/get-caller-file": { 890 | "version": "2.0.5", 891 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 892 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 893 | "engines": { 894 | "node": "6.* || 8.* || >= 10.*" 895 | } 896 | }, 897 | "node_modules/get-intrinsic": { 898 | "version": "1.2.4", 899 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", 900 | "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", 901 | "dependencies": { 902 | "es-errors": "^1.3.0", 903 | "function-bind": "^1.1.2", 904 | "has-proto": "^1.0.1", 905 | "has-symbols": "^1.0.3", 906 | "hasown": "^2.0.0" 907 | }, 908 | "engines": { 909 | "node": ">= 0.4" 910 | }, 911 | "funding": { 912 | "url": "https://github.com/sponsors/ljharb" 913 | } 914 | }, 915 | "node_modules/glob-parent": { 916 | "version": "5.1.2", 917 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 918 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 919 | "dev": true, 920 | "dependencies": { 921 | "is-glob": "^4.0.1" 922 | }, 923 | "engines": { 924 | "node": ">= 6" 925 | } 926 | }, 927 | "node_modules/gopd": { 928 | "version": "1.0.1", 929 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", 930 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", 931 | "dependencies": { 932 | "get-intrinsic": "^1.1.3" 933 | }, 934 | "funding": { 935 | "url": "https://github.com/sponsors/ljharb" 936 | } 937 | }, 938 | "node_modules/has-flag": { 939 | "version": "3.0.0", 940 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 941 | "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", 942 | "dev": true, 943 | "engines": { 944 | "node": ">=4" 945 | } 946 | }, 947 | "node_modules/has-property-descriptors": { 948 | "version": "1.0.2", 949 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", 950 | "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", 951 | "dependencies": { 952 | "es-define-property": "^1.0.0" 953 | }, 954 | "funding": { 955 | "url": "https://github.com/sponsors/ljharb" 956 | } 957 | }, 958 | "node_modules/has-proto": { 959 | "version": "1.0.3", 960 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", 961 | "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", 962 | "engines": { 963 | "node": ">= 0.4" 964 | }, 965 | "funding": { 966 | "url": "https://github.com/sponsors/ljharb" 967 | } 968 | }, 969 | "node_modules/has-symbols": { 970 | "version": "1.0.3", 971 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 972 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 973 | "engines": { 974 | "node": ">= 0.4" 975 | }, 976 | "funding": { 977 | "url": "https://github.com/sponsors/ljharb" 978 | } 979 | }, 980 | "node_modules/hasown": { 981 | "version": "2.0.2", 982 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 983 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 984 | "dependencies": { 985 | "function-bind": "^1.1.2" 986 | }, 987 | "engines": { 988 | "node": ">= 0.4" 989 | } 990 | }, 991 | "node_modules/http-errors": { 992 | "version": "2.0.0", 993 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 994 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 995 | "dependencies": { 996 | "depd": "2.0.0", 997 | "inherits": "2.0.4", 998 | "setprototypeof": "1.2.0", 999 | "statuses": "2.0.1", 1000 | "toidentifier": "1.0.1" 1001 | }, 1002 | "engines": { 1003 | "node": ">= 0.8" 1004 | } 1005 | }, 1006 | "node_modules/humanize-ms": { 1007 | "version": "1.2.1", 1008 | "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", 1009 | "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", 1010 | "dependencies": { 1011 | "ms": "^2.0.0" 1012 | } 1013 | }, 1014 | "node_modules/iconv-lite": { 1015 | "version": "0.4.24", 1016 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 1017 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 1018 | "dependencies": { 1019 | "safer-buffer": ">= 2.1.2 < 3" 1020 | }, 1021 | "engines": { 1022 | "node": ">=0.10.0" 1023 | } 1024 | }, 1025 | "node_modules/ieee754": { 1026 | "version": "1.2.1", 1027 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 1028 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 1029 | "funding": [ 1030 | { 1031 | "type": "github", 1032 | "url": "https://github.com/sponsors/feross" 1033 | }, 1034 | { 1035 | "type": "patreon", 1036 | "url": "https://www.patreon.com/feross" 1037 | }, 1038 | { 1039 | "type": "consulting", 1040 | "url": "https://feross.org/support" 1041 | } 1042 | ] 1043 | }, 1044 | "node_modules/ignore-by-default": { 1045 | "version": "1.0.1", 1046 | "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", 1047 | "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", 1048 | "dev": true 1049 | }, 1050 | "node_modules/inherits": { 1051 | "version": "2.0.4", 1052 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1053 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 1054 | }, 1055 | "node_modules/ipaddr.js": { 1056 | "version": "1.9.1", 1057 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 1058 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 1059 | "engines": { 1060 | "node": ">= 0.10" 1061 | } 1062 | }, 1063 | "node_modules/is-binary-path": { 1064 | "version": "2.1.0", 1065 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 1066 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 1067 | "dev": true, 1068 | "dependencies": { 1069 | "binary-extensions": "^2.0.0" 1070 | }, 1071 | "engines": { 1072 | "node": ">=8" 1073 | } 1074 | }, 1075 | "node_modules/is-extglob": { 1076 | "version": "2.1.1", 1077 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 1078 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 1079 | "dev": true, 1080 | "engines": { 1081 | "node": ">=0.10.0" 1082 | } 1083 | }, 1084 | "node_modules/is-fullwidth-code-point": { 1085 | "version": "3.0.0", 1086 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 1087 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 1088 | "engines": { 1089 | "node": ">=8" 1090 | } 1091 | }, 1092 | "node_modules/is-glob": { 1093 | "version": "4.0.3", 1094 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 1095 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 1096 | "dev": true, 1097 | "dependencies": { 1098 | "is-extglob": "^2.1.1" 1099 | }, 1100 | "engines": { 1101 | "node": ">=0.10.0" 1102 | } 1103 | }, 1104 | "node_modules/is-number": { 1105 | "version": "7.0.0", 1106 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 1107 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 1108 | "dev": true, 1109 | "engines": { 1110 | "node": ">=0.12.0" 1111 | } 1112 | }, 1113 | "node_modules/is-typedarray": { 1114 | "version": "1.0.0", 1115 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 1116 | "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" 1117 | }, 1118 | "node_modules/lodash.camelcase": { 1119 | "version": "4.3.0", 1120 | "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", 1121 | "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" 1122 | }, 1123 | "node_modules/long": { 1124 | "version": "5.2.3", 1125 | "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", 1126 | "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" 1127 | }, 1128 | "node_modules/media-typer": { 1129 | "version": "0.3.0", 1130 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 1131 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 1132 | "engines": { 1133 | "node": ">= 0.6" 1134 | } 1135 | }, 1136 | "node_modules/merge-descriptors": { 1137 | "version": "1.0.1", 1138 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 1139 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" 1140 | }, 1141 | "node_modules/methods": { 1142 | "version": "1.1.2", 1143 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 1144 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 1145 | "engines": { 1146 | "node": ">= 0.6" 1147 | } 1148 | }, 1149 | "node_modules/mime": { 1150 | "version": "1.6.0", 1151 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 1152 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 1153 | "bin": { 1154 | "mime": "cli.js" 1155 | }, 1156 | "engines": { 1157 | "node": ">=4" 1158 | } 1159 | }, 1160 | "node_modules/mime-db": { 1161 | "version": "1.52.0", 1162 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 1163 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 1164 | "engines": { 1165 | "node": ">= 0.6" 1166 | } 1167 | }, 1168 | "node_modules/mime-types": { 1169 | "version": "2.1.35", 1170 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 1171 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 1172 | "dependencies": { 1173 | "mime-db": "1.52.0" 1174 | }, 1175 | "engines": { 1176 | "node": ">= 0.6" 1177 | } 1178 | }, 1179 | "node_modules/minimatch": { 1180 | "version": "3.1.2", 1181 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 1182 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 1183 | "dev": true, 1184 | "dependencies": { 1185 | "brace-expansion": "^1.1.7" 1186 | }, 1187 | "engines": { 1188 | "node": "*" 1189 | } 1190 | }, 1191 | "node_modules/ms": { 1192 | "version": "2.0.0", 1193 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 1194 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 1195 | }, 1196 | "node_modules/negotiator": { 1197 | "version": "0.6.3", 1198 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 1199 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 1200 | "engines": { 1201 | "node": ">= 0.6" 1202 | } 1203 | }, 1204 | "node_modules/next-tick": { 1205 | "version": "1.1.0", 1206 | "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", 1207 | "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" 1208 | }, 1209 | "node_modules/node-domexception": { 1210 | "version": "1.0.0", 1211 | "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", 1212 | "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", 1213 | "funding": [ 1214 | { 1215 | "type": "github", 1216 | "url": "https://github.com/sponsors/jimmywarting" 1217 | }, 1218 | { 1219 | "type": "github", 1220 | "url": "https://paypal.me/jimmywarting" 1221 | } 1222 | ], 1223 | "engines": { 1224 | "node": ">=10.5.0" 1225 | } 1226 | }, 1227 | "node_modules/node-fetch": { 1228 | "version": "2.7.0", 1229 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", 1230 | "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", 1231 | "dependencies": { 1232 | "whatwg-url": "^5.0.0" 1233 | }, 1234 | "engines": { 1235 | "node": "4.x || >=6.0.0" 1236 | }, 1237 | "peerDependencies": { 1238 | "encoding": "^0.1.0" 1239 | }, 1240 | "peerDependenciesMeta": { 1241 | "encoding": { 1242 | "optional": true 1243 | } 1244 | } 1245 | }, 1246 | "node_modules/node-gyp-build": { 1247 | "version": "4.8.1", 1248 | "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", 1249 | "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", 1250 | "bin": { 1251 | "node-gyp-build": "bin.js", 1252 | "node-gyp-build-optional": "optional.js", 1253 | "node-gyp-build-test": "build-test.js" 1254 | } 1255 | }, 1256 | "node_modules/nodemon": { 1257 | "version": "3.1.4", 1258 | "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.4.tgz", 1259 | "integrity": "sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==", 1260 | "dev": true, 1261 | "dependencies": { 1262 | "chokidar": "^3.5.2", 1263 | "debug": "^4", 1264 | "ignore-by-default": "^1.0.1", 1265 | "minimatch": "^3.1.2", 1266 | "pstree.remy": "^1.1.8", 1267 | "semver": "^7.5.3", 1268 | "simple-update-notifier": "^2.0.0", 1269 | "supports-color": "^5.5.0", 1270 | "touch": "^3.1.0", 1271 | "undefsafe": "^2.0.5" 1272 | }, 1273 | "bin": { 1274 | "nodemon": "bin/nodemon.js" 1275 | }, 1276 | "engines": { 1277 | "node": ">=10" 1278 | }, 1279 | "funding": { 1280 | "type": "opencollective", 1281 | "url": "https://opencollective.com/nodemon" 1282 | } 1283 | }, 1284 | "node_modules/nodemon/node_modules/debug": { 1285 | "version": "4.3.5", 1286 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", 1287 | "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", 1288 | "dev": true, 1289 | "dependencies": { 1290 | "ms": "2.1.2" 1291 | }, 1292 | "engines": { 1293 | "node": ">=6.0" 1294 | }, 1295 | "peerDependenciesMeta": { 1296 | "supports-color": { 1297 | "optional": true 1298 | } 1299 | } 1300 | }, 1301 | "node_modules/nodemon/node_modules/ms": { 1302 | "version": "2.1.2", 1303 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 1304 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 1305 | "dev": true 1306 | }, 1307 | "node_modules/normalize-path": { 1308 | "version": "3.0.0", 1309 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 1310 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 1311 | "dev": true, 1312 | "engines": { 1313 | "node": ">=0.10.0" 1314 | } 1315 | }, 1316 | "node_modules/object-inspect": { 1317 | "version": "1.13.1", 1318 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", 1319 | "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", 1320 | "funding": { 1321 | "url": "https://github.com/sponsors/ljharb" 1322 | } 1323 | }, 1324 | "node_modules/on-finished": { 1325 | "version": "2.4.1", 1326 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 1327 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 1328 | "dependencies": { 1329 | "ee-first": "1.1.1" 1330 | }, 1331 | "engines": { 1332 | "node": ">= 0.8" 1333 | } 1334 | }, 1335 | "node_modules/openai": { 1336 | "version": "4.52.0", 1337 | "resolved": "https://registry.npmjs.org/openai/-/openai-4.52.0.tgz", 1338 | "integrity": "sha512-xmiNcdA9QJ5wffHpZDpIsge6AsPTETJ6h5iqDNuFQ7qGSNtonHn8Qe0VHy4UwLE8rBWiSqh4j+iSvuYZSeKkPg==", 1339 | "dependencies": { 1340 | "@types/node": "^18.11.18", 1341 | "@types/node-fetch": "^2.6.4", 1342 | "abort-controller": "^3.0.0", 1343 | "agentkeepalive": "^4.2.1", 1344 | "form-data-encoder": "1.7.2", 1345 | "formdata-node": "^4.3.2", 1346 | "node-fetch": "^2.6.7", 1347 | "web-streams-polyfill": "^3.2.1" 1348 | }, 1349 | "bin": { 1350 | "openai": "bin/cli" 1351 | } 1352 | }, 1353 | "node_modules/parseurl": { 1354 | "version": "1.3.3", 1355 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 1356 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 1357 | "engines": { 1358 | "node": ">= 0.8" 1359 | } 1360 | }, 1361 | "node_modules/path-to-regexp": { 1362 | "version": "0.1.7", 1363 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 1364 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" 1365 | }, 1366 | "node_modules/peek-readable": { 1367 | "version": "5.0.0", 1368 | "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", 1369 | "integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==", 1370 | "engines": { 1371 | "node": ">=14.16" 1372 | }, 1373 | "funding": { 1374 | "type": "github", 1375 | "url": "https://github.com/sponsors/Borewit" 1376 | } 1377 | }, 1378 | "node_modules/picomatch": { 1379 | "version": "2.3.1", 1380 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 1381 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 1382 | "dev": true, 1383 | "engines": { 1384 | "node": ">=8.6" 1385 | }, 1386 | "funding": { 1387 | "url": "https://github.com/sponsors/jonschlinkert" 1388 | } 1389 | }, 1390 | "node_modules/playht": { 1391 | "version": "0.9.8", 1392 | "resolved": "https://registry.npmjs.org/playht/-/playht-0.9.8.tgz", 1393 | "integrity": "sha512-JHwBOk6WrW+BntYb6WXqhNMXjkUjhFiPiQ3CwgzpmLRd9+FFFs0A5dJ84DFy3JOnyvDsXKQd7pP/4Ohd+e015w==", 1394 | "dependencies": { 1395 | "@grpc/grpc-js": "^1.9.4", 1396 | "axios": "^1.4.0", 1397 | "cross-fetch": "^4.0.0", 1398 | "file-type": "^18.5.0", 1399 | "protobufjs": "^7.2.5", 1400 | "tslib": "^2.1.0" 1401 | } 1402 | }, 1403 | "node_modules/protobufjs": { 1404 | "version": "7.3.2", 1405 | "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.2.tgz", 1406 | "integrity": "sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==", 1407 | "hasInstallScript": true, 1408 | "dependencies": { 1409 | "@protobufjs/aspromise": "^1.1.2", 1410 | "@protobufjs/base64": "^1.1.2", 1411 | "@protobufjs/codegen": "^2.0.4", 1412 | "@protobufjs/eventemitter": "^1.1.0", 1413 | "@protobufjs/fetch": "^1.1.0", 1414 | "@protobufjs/float": "^1.0.2", 1415 | "@protobufjs/inquire": "^1.1.0", 1416 | "@protobufjs/path": "^1.1.2", 1417 | "@protobufjs/pool": "^1.1.0", 1418 | "@protobufjs/utf8": "^1.1.0", 1419 | "@types/node": ">=13.7.0", 1420 | "long": "^5.0.0" 1421 | }, 1422 | "engines": { 1423 | "node": ">=12.0.0" 1424 | } 1425 | }, 1426 | "node_modules/proxy-addr": { 1427 | "version": "2.0.7", 1428 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 1429 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 1430 | "dependencies": { 1431 | "forwarded": "0.2.0", 1432 | "ipaddr.js": "1.9.1" 1433 | }, 1434 | "engines": { 1435 | "node": ">= 0.10" 1436 | } 1437 | }, 1438 | "node_modules/proxy-from-env": { 1439 | "version": "1.1.0", 1440 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 1441 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" 1442 | }, 1443 | "node_modules/pstree.remy": { 1444 | "version": "1.1.8", 1445 | "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", 1446 | "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", 1447 | "dev": true 1448 | }, 1449 | "node_modules/qs": { 1450 | "version": "6.11.0", 1451 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", 1452 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", 1453 | "dependencies": { 1454 | "side-channel": "^1.0.4" 1455 | }, 1456 | "engines": { 1457 | "node": ">=0.6" 1458 | }, 1459 | "funding": { 1460 | "url": "https://github.com/sponsors/ljharb" 1461 | } 1462 | }, 1463 | "node_modules/range-parser": { 1464 | "version": "1.2.1", 1465 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 1466 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 1467 | "engines": { 1468 | "node": ">= 0.6" 1469 | } 1470 | }, 1471 | "node_modules/raw-body": { 1472 | "version": "2.5.2", 1473 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", 1474 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", 1475 | "dependencies": { 1476 | "bytes": "3.1.2", 1477 | "http-errors": "2.0.0", 1478 | "iconv-lite": "0.4.24", 1479 | "unpipe": "1.0.0" 1480 | }, 1481 | "engines": { 1482 | "node": ">= 0.8" 1483 | } 1484 | }, 1485 | "node_modules/readable-stream": { 1486 | "version": "3.6.2", 1487 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", 1488 | "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", 1489 | "dependencies": { 1490 | "inherits": "^2.0.3", 1491 | "string_decoder": "^1.1.1", 1492 | "util-deprecate": "^1.0.1" 1493 | }, 1494 | "engines": { 1495 | "node": ">= 6" 1496 | } 1497 | }, 1498 | "node_modules/readable-web-to-node-stream": { 1499 | "version": "3.0.2", 1500 | "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", 1501 | "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", 1502 | "dependencies": { 1503 | "readable-stream": "^3.6.0" 1504 | }, 1505 | "engines": { 1506 | "node": ">=8" 1507 | }, 1508 | "funding": { 1509 | "type": "github", 1510 | "url": "https://github.com/sponsors/Borewit" 1511 | } 1512 | }, 1513 | "node_modules/readdirp": { 1514 | "version": "3.6.0", 1515 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 1516 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 1517 | "dev": true, 1518 | "dependencies": { 1519 | "picomatch": "^2.2.1" 1520 | }, 1521 | "engines": { 1522 | "node": ">=8.10.0" 1523 | } 1524 | }, 1525 | "node_modules/require-directory": { 1526 | "version": "2.1.1", 1527 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 1528 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 1529 | "engines": { 1530 | "node": ">=0.10.0" 1531 | } 1532 | }, 1533 | "node_modules/safe-buffer": { 1534 | "version": "5.2.1", 1535 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1536 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 1537 | "funding": [ 1538 | { 1539 | "type": "github", 1540 | "url": "https://github.com/sponsors/feross" 1541 | }, 1542 | { 1543 | "type": "patreon", 1544 | "url": "https://www.patreon.com/feross" 1545 | }, 1546 | { 1547 | "type": "consulting", 1548 | "url": "https://feross.org/support" 1549 | } 1550 | ] 1551 | }, 1552 | "node_modules/safer-buffer": { 1553 | "version": "2.1.2", 1554 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1555 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1556 | }, 1557 | "node_modules/semver": { 1558 | "version": "7.6.2", 1559 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", 1560 | "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", 1561 | "dev": true, 1562 | "bin": { 1563 | "semver": "bin/semver.js" 1564 | }, 1565 | "engines": { 1566 | "node": ">=10" 1567 | } 1568 | }, 1569 | "node_modules/send": { 1570 | "version": "0.18.0", 1571 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", 1572 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", 1573 | "dependencies": { 1574 | "debug": "2.6.9", 1575 | "depd": "2.0.0", 1576 | "destroy": "1.2.0", 1577 | "encodeurl": "~1.0.2", 1578 | "escape-html": "~1.0.3", 1579 | "etag": "~1.8.1", 1580 | "fresh": "0.5.2", 1581 | "http-errors": "2.0.0", 1582 | "mime": "1.6.0", 1583 | "ms": "2.1.3", 1584 | "on-finished": "2.4.1", 1585 | "range-parser": "~1.2.1", 1586 | "statuses": "2.0.1" 1587 | }, 1588 | "engines": { 1589 | "node": ">= 0.8.0" 1590 | } 1591 | }, 1592 | "node_modules/send/node_modules/ms": { 1593 | "version": "2.1.3", 1594 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1595 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 1596 | }, 1597 | "node_modules/serve-static": { 1598 | "version": "1.15.0", 1599 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", 1600 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", 1601 | "dependencies": { 1602 | "encodeurl": "~1.0.2", 1603 | "escape-html": "~1.0.3", 1604 | "parseurl": "~1.3.3", 1605 | "send": "0.18.0" 1606 | }, 1607 | "engines": { 1608 | "node": ">= 0.8.0" 1609 | } 1610 | }, 1611 | "node_modules/set-function-length": { 1612 | "version": "1.2.2", 1613 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", 1614 | "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", 1615 | "dependencies": { 1616 | "define-data-property": "^1.1.4", 1617 | "es-errors": "^1.3.0", 1618 | "function-bind": "^1.1.2", 1619 | "get-intrinsic": "^1.2.4", 1620 | "gopd": "^1.0.1", 1621 | "has-property-descriptors": "^1.0.2" 1622 | }, 1623 | "engines": { 1624 | "node": ">= 0.4" 1625 | } 1626 | }, 1627 | "node_modules/setprototypeof": { 1628 | "version": "1.2.0", 1629 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 1630 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 1631 | }, 1632 | "node_modules/side-channel": { 1633 | "version": "1.0.6", 1634 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", 1635 | "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", 1636 | "dependencies": { 1637 | "call-bind": "^1.0.7", 1638 | "es-errors": "^1.3.0", 1639 | "get-intrinsic": "^1.2.4", 1640 | "object-inspect": "^1.13.1" 1641 | }, 1642 | "engines": { 1643 | "node": ">= 0.4" 1644 | }, 1645 | "funding": { 1646 | "url": "https://github.com/sponsors/ljharb" 1647 | } 1648 | }, 1649 | "node_modules/simple-update-notifier": { 1650 | "version": "2.0.0", 1651 | "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", 1652 | "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", 1653 | "dev": true, 1654 | "dependencies": { 1655 | "semver": "^7.5.3" 1656 | }, 1657 | "engines": { 1658 | "node": ">=10" 1659 | } 1660 | }, 1661 | "node_modules/statuses": { 1662 | "version": "2.0.1", 1663 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 1664 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 1665 | "engines": { 1666 | "node": ">= 0.8" 1667 | } 1668 | }, 1669 | "node_modules/string_decoder": { 1670 | "version": "1.3.0", 1671 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 1672 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 1673 | "dependencies": { 1674 | "safe-buffer": "~5.2.0" 1675 | } 1676 | }, 1677 | "node_modules/string-width": { 1678 | "version": "4.2.3", 1679 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1680 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1681 | "dependencies": { 1682 | "emoji-regex": "^8.0.0", 1683 | "is-fullwidth-code-point": "^3.0.0", 1684 | "strip-ansi": "^6.0.1" 1685 | }, 1686 | "engines": { 1687 | "node": ">=8" 1688 | } 1689 | }, 1690 | "node_modules/strip-ansi": { 1691 | "version": "6.0.1", 1692 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1693 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1694 | "dependencies": { 1695 | "ansi-regex": "^5.0.1" 1696 | }, 1697 | "engines": { 1698 | "node": ">=8" 1699 | } 1700 | }, 1701 | "node_modules/strtok3": { 1702 | "version": "7.0.0", 1703 | "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz", 1704 | "integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==", 1705 | "dependencies": { 1706 | "@tokenizer/token": "^0.3.0", 1707 | "peek-readable": "^5.0.0" 1708 | }, 1709 | "engines": { 1710 | "node": ">=14.16" 1711 | }, 1712 | "funding": { 1713 | "type": "github", 1714 | "url": "https://github.com/sponsors/Borewit" 1715 | } 1716 | }, 1717 | "node_modules/supports-color": { 1718 | "version": "5.5.0", 1719 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 1720 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 1721 | "dev": true, 1722 | "dependencies": { 1723 | "has-flag": "^3.0.0" 1724 | }, 1725 | "engines": { 1726 | "node": ">=4" 1727 | } 1728 | }, 1729 | "node_modules/to-regex-range": { 1730 | "version": "5.0.1", 1731 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1732 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1733 | "dev": true, 1734 | "dependencies": { 1735 | "is-number": "^7.0.0" 1736 | }, 1737 | "engines": { 1738 | "node": ">=8.0" 1739 | } 1740 | }, 1741 | "node_modules/toidentifier": { 1742 | "version": "1.0.1", 1743 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 1744 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 1745 | "engines": { 1746 | "node": ">=0.6" 1747 | } 1748 | }, 1749 | "node_modules/token-types": { 1750 | "version": "5.0.1", 1751 | "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", 1752 | "integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==", 1753 | "dependencies": { 1754 | "@tokenizer/token": "^0.3.0", 1755 | "ieee754": "^1.2.1" 1756 | }, 1757 | "engines": { 1758 | "node": ">=14.16" 1759 | }, 1760 | "funding": { 1761 | "type": "github", 1762 | "url": "https://github.com/sponsors/Borewit" 1763 | } 1764 | }, 1765 | "node_modules/touch": { 1766 | "version": "3.1.1", 1767 | "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", 1768 | "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", 1769 | "dev": true, 1770 | "bin": { 1771 | "nodetouch": "bin/nodetouch.js" 1772 | } 1773 | }, 1774 | "node_modules/tr46": { 1775 | "version": "0.0.3", 1776 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 1777 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" 1778 | }, 1779 | "node_modules/tslib": { 1780 | "version": "2.6.3", 1781 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", 1782 | "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" 1783 | }, 1784 | "node_modules/type": { 1785 | "version": "2.7.3", 1786 | "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", 1787 | "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==" 1788 | }, 1789 | "node_modules/type-is": { 1790 | "version": "1.6.18", 1791 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1792 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1793 | "dependencies": { 1794 | "media-typer": "0.3.0", 1795 | "mime-types": "~2.1.24" 1796 | }, 1797 | "engines": { 1798 | "node": ">= 0.6" 1799 | } 1800 | }, 1801 | "node_modules/typedarray-to-buffer": { 1802 | "version": "3.1.5", 1803 | "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", 1804 | "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", 1805 | "dependencies": { 1806 | "is-typedarray": "^1.0.0" 1807 | } 1808 | }, 1809 | "node_modules/undefsafe": { 1810 | "version": "2.0.5", 1811 | "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", 1812 | "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", 1813 | "dev": true 1814 | }, 1815 | "node_modules/undici-types": { 1816 | "version": "5.26.5", 1817 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 1818 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" 1819 | }, 1820 | "node_modules/unpipe": { 1821 | "version": "1.0.0", 1822 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1823 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 1824 | "engines": { 1825 | "node": ">= 0.8" 1826 | } 1827 | }, 1828 | "node_modules/utf-8-validate": { 1829 | "version": "5.0.10", 1830 | "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", 1831 | "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", 1832 | "hasInstallScript": true, 1833 | "dependencies": { 1834 | "node-gyp-build": "^4.3.0" 1835 | }, 1836 | "engines": { 1837 | "node": ">=6.14.2" 1838 | } 1839 | }, 1840 | "node_modules/util-deprecate": { 1841 | "version": "1.0.2", 1842 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1843 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" 1844 | }, 1845 | "node_modules/utils-merge": { 1846 | "version": "1.0.1", 1847 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1848 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 1849 | "engines": { 1850 | "node": ">= 0.4.0" 1851 | } 1852 | }, 1853 | "node_modules/vary": { 1854 | "version": "1.1.2", 1855 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1856 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 1857 | "engines": { 1858 | "node": ">= 0.8" 1859 | } 1860 | }, 1861 | "node_modules/web-streams-polyfill": { 1862 | "version": "3.3.3", 1863 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", 1864 | "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", 1865 | "engines": { 1866 | "node": ">= 8" 1867 | } 1868 | }, 1869 | "node_modules/webidl-conversions": { 1870 | "version": "3.0.1", 1871 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 1872 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" 1873 | }, 1874 | "node_modules/websocket": { 1875 | "version": "1.0.35", 1876 | "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.35.tgz", 1877 | "integrity": "sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==", 1878 | "dependencies": { 1879 | "bufferutil": "^4.0.1", 1880 | "debug": "^2.2.0", 1881 | "es5-ext": "^0.10.63", 1882 | "typedarray-to-buffer": "^3.1.5", 1883 | "utf-8-validate": "^5.0.2", 1884 | "yaeti": "^0.0.6" 1885 | }, 1886 | "engines": { 1887 | "node": ">=4.0.0" 1888 | } 1889 | }, 1890 | "node_modules/whatwg-url": { 1891 | "version": "5.0.0", 1892 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 1893 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 1894 | "dependencies": { 1895 | "tr46": "~0.0.3", 1896 | "webidl-conversions": "^3.0.0" 1897 | } 1898 | }, 1899 | "node_modules/wrap-ansi": { 1900 | "version": "7.0.0", 1901 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 1902 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 1903 | "dependencies": { 1904 | "ansi-styles": "^4.0.0", 1905 | "string-width": "^4.1.0", 1906 | "strip-ansi": "^6.0.0" 1907 | }, 1908 | "engines": { 1909 | "node": ">=10" 1910 | }, 1911 | "funding": { 1912 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 1913 | } 1914 | }, 1915 | "node_modules/ws": { 1916 | "version": "8.17.1", 1917 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", 1918 | "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", 1919 | "engines": { 1920 | "node": ">=10.0.0" 1921 | }, 1922 | "peerDependencies": { 1923 | "bufferutil": "^4.0.1", 1924 | "utf-8-validate": ">=5.0.2" 1925 | }, 1926 | "peerDependenciesMeta": { 1927 | "bufferutil": { 1928 | "optional": true 1929 | }, 1930 | "utf-8-validate": { 1931 | "optional": true 1932 | } 1933 | } 1934 | }, 1935 | "node_modules/y18n": { 1936 | "version": "5.0.8", 1937 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 1938 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 1939 | "engines": { 1940 | "node": ">=10" 1941 | } 1942 | }, 1943 | "node_modules/yaeti": { 1944 | "version": "0.0.6", 1945 | "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", 1946 | "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==", 1947 | "engines": { 1948 | "node": ">=0.10.32" 1949 | } 1950 | }, 1951 | "node_modules/yargs": { 1952 | "version": "17.7.2", 1953 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", 1954 | "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", 1955 | "dependencies": { 1956 | "cliui": "^8.0.1", 1957 | "escalade": "^3.1.1", 1958 | "get-caller-file": "^2.0.5", 1959 | "require-directory": "^2.1.1", 1960 | "string-width": "^4.2.3", 1961 | "y18n": "^5.0.5", 1962 | "yargs-parser": "^21.1.1" 1963 | }, 1964 | "engines": { 1965 | "node": ">=12" 1966 | } 1967 | }, 1968 | "node_modules/yargs-parser": { 1969 | "version": "21.1.1", 1970 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", 1971 | "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", 1972 | "engines": { 1973 | "node": ">=12" 1974 | } 1975 | } 1976 | } 1977 | } 1978 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "dev": "nodemon index.js" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@deepgram/sdk": "^3.3.5", 15 | "dotenv": "^16.4.5", 16 | "express": "^4.19.2", 17 | "node-fetch": "^2.7.0", 18 | "openai": "^4.52.0", 19 | "playht": "^0.9.8", 20 | "ws": "^8.17.1" 21 | }, 22 | "devDependencies": { 23 | "nodemon": "^3.1.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /web/.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 | .vercel 25 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /web/config-overrides.js: -------------------------------------------------------------------------------- 1 | const CopyPlugin = require("copy-webpack-plugin") 2 | 3 | module.exports = function override(config, env) { 4 | // ... 5 | config.plugins.push( 6 | // ... 7 | new CopyPlugin({ 8 | patterns: [ 9 | // ... 10 | { 11 | from: "node_modules/@ricky0123/vad-web/dist/vad.worklet.bundle.min.js", 12 | to: "static/js/[name][ext]", 13 | }, 14 | { 15 | from: "node_modules/@ricky0123/vad-web/dist/*.onnx", 16 | to: "static/js/[name][ext]", 17 | }, 18 | { from: "node_modules/onnxruntime-web/dist/*.wasm", to: "static/js/[name][ext]" }, 19 | ], 20 | }), 21 | ) 22 | return config; 23 | } 24 | 25 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@ricky0123/vad-web": "^0.0.18", 7 | "@testing-library/jest-dom": "^5.17.0", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "@types/jest": "^27.5.2", 11 | "@types/node": "^16.18.101", 12 | "@types/react": "^18.3.3", 13 | "@types/react-dom": "^18.3.0", 14 | "@vercel/analytics": "^1.3.1", 15 | "react": "^18.3.1", 16 | "react-dom": "^18.3.1", 17 | "react-scripts": "5.0.1", 18 | "typescript": "^4.9.5", 19 | "web-vitals": "^2.1.4" 20 | }, 21 | "scripts": { 22 | "start": "react-app-rewired start", 23 | "build": "react-app-rewired build", 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 | "copy-webpack-plugin": "^12.0.2", 46 | "react-app-rewired": "^2.2.1", 47 | "tailwindcss": "^3.4.4" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /web/public/GitHub-Logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/botany-labs/voice-ai-js-starter/2640781dee8faf68e8079387cf5785ae7ba07807/web/public/favicon.ico -------------------------------------------------------------------------------- /web/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 | -------------------------------------------------------------------------------- /web/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/botany-labs/voice-ai-js-starter/2640781dee8faf68e8079387cf5785ae7ba07807/web/public/logo192.png -------------------------------------------------------------------------------- /web/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/botany-labs/voice-ai-js-starter/2640781dee8faf68e8079387cf5785ae7ba07807/web/public/logo512.png -------------------------------------------------------------------------------- /web/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 | -------------------------------------------------------------------------------- /web/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /web/src/app.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef, useEffect } from "react"; 2 | import * as vad from "@ricky0123/vad-web"; 3 | import EventEmitter from "events"; 4 | 5 | const SERVER_WS_URL = 6 | process.env.REACT_APP_SERVER_WS_URL ?? "ws://localhost:8000"; 7 | 8 | const START_LISTENING_TOKEN = "RDY"; // Sent by server to indicate start VAD 9 | const END_OF_SPEECH_TOKEN = "EOS"; // End of speech on client side 10 | const INTERRUPT_TOKEN = "INT"; // Interrupt reported from client side 11 | const CLEAR_BUFFER_TOKEN = "CLR"; // Clear playback buffer request from server 12 | 13 | // These are shared between streamer and playback but 14 | // we are using float32arrays of pcm 24k 16bit mono 15 | const AudioContextSettings = { 16 | sampleRate: 24000, 17 | bitDepth: 16, 18 | numChannels: 1, 19 | echoCancellation: true, 20 | autoGainControl: true, 21 | noiseSuppression: true, 22 | channelCount: 1, 23 | }; 24 | 25 | export default function App() { 26 | const [logMessage, Logs] = useLogs(); 27 | const ws = useRef(null); 28 | const [isRecording, setIsRecording] = useState(false); 29 | const streamer = useRef(null); 30 | const playback = useRef(null); 31 | const lastEOS = useRef(null); 32 | const [assistant, setAssistant] = useState< 33 | "fastest" | "best-quality" | "openai" 34 | >("fastest"); 35 | 36 | const stopRecording = (graceful: boolean = false) => { 37 | setIsRecording(false); 38 | streamer.current?.stop(graceful); 39 | playback.current?.stop(graceful); 40 | ws.current?.close(); 41 | ws.current = null; 42 | lastEOS.current = null; 43 | }; 44 | 45 | const startRecording = async () => { 46 | setIsRecording(true); 47 | if (!ws.current || ws.current.readyState !== WebSocket.OPEN) { 48 | ws.current = new WebSocket( 49 | SERVER_WS_URL + "?assistant=" + (assistant || "default") 50 | ); 51 | ws.current.binaryType = "arraybuffer"; 52 | ws.current.onopen = () => { 53 | ws.current && 54 | (ws.current.onmessage = (event) => { 55 | if (event.data instanceof ArrayBuffer) { 56 | playback.current?.addSamples(new Float32Array(event.data)); 57 | } else if (event.data === CLEAR_BUFFER_TOKEN) { 58 | playback.current?.clear().then((didInterrupt: boolean) => { 59 | if (didInterrupt) { 60 | logMessage("--- interrupt recorded", didInterrupt); 61 | ws.current && ws.current.send(INTERRUPT_TOKEN); 62 | } 63 | }); 64 | } else if (event.data === START_LISTENING_TOKEN) { 65 | playback.current?.once("playbackEnd", () => { 66 | logMessage("--- starting vad"); 67 | streamer.current?.startVoiceDetection(); 68 | }); 69 | } else { 70 | logMessage(event.data); 71 | } 72 | }); 73 | 74 | logMessage("start recording", new Date()); 75 | playback.current = new Playback(new AudioContext(AudioContextSettings)); 76 | playback.current.on("playbackStart", () => { 77 | if (!lastEOS.current) { 78 | return; 79 | } 80 | const responseTime = new Date().getTime() - lastEOS.current.getTime(); 81 | logMessage("--- time.TOTAL_RESPONSE ", responseTime, " ms"); 82 | }); 83 | playback.current.start(); 84 | streamer.current = new Streamer(ws.current!, logMessage); 85 | streamer.current.on("speechStart", () => { 86 | playback.current?.clear().then((didInterrupt: boolean) => { 87 | if (didInterrupt) { 88 | logMessage("--- interrupt recorded", didInterrupt); 89 | ws.current && ws.current.send(INTERRUPT_TOKEN); 90 | } 91 | }); 92 | }); 93 | streamer.current.on("speechEnd", () => { 94 | lastEOS.current = new Date(); 95 | }); 96 | streamer.current.start(); 97 | 98 | ws.current && 99 | (ws.current.onclose = () => { 100 | logMessage("websocket closed"); 101 | stopRecording(true); 102 | }); 103 | }; 104 | 105 | ws.current.onerror = (event) => { 106 | logMessage("websocket error", event); 107 | }; 108 | } 109 | }; 110 | 111 | return ( 112 |
113 | 130 |
131 | {isRecording ? ( 132 | 138 | ) : ( 139 | 145 | )} 146 |
147 |
Configuration:
148 | 160 |
161 |
162 | {assistant === "fastest" && ( 163 | <> 164 | {" "} 165 | TTS: Deepgram Nova-2 Streaming / STT: Deepgram Aura / LLM: ChatGPT 166 | 3.5 Turbo{" "} 167 | 168 | )} 169 | {assistant === "best-quality" && ( 170 | <> 171 | {" "} 172 | TTS: OpenAI Whisper / STT: Elevenlabs Turbo V2 / LLM: ChatGPT 3.5 173 | Turbo{" "} 174 | 175 | )} 176 | {assistant === "openai" && ( 177 | <> 178 | {" "} 179 | TTS: OpenAI Whisper / STT: OpenAI TTS-1 / LLM: ChatGPT 3.5 Turbo{" "} 180 | 181 | )} 182 |
183 |
184 | 185 |
186 | ); 187 | } 188 | 189 | const Logs = ({ 190 | logLines, 191 | clearLogs, 192 | }: { 193 | logLines: JSX.Element[]; 194 | clearLogs: () => void; 195 | }) => { 196 | return ( 197 | <> 198 |
199 |

Logs

200 | 207 |
208 |
209 | {logLines.map((line, index) => ( 210 |

{line}

211 | ))} 212 |
213 | 214 | ); 215 | }; 216 | 217 | const useLogs = () => { 218 | const [logs, setLogs] = useState<{ time: Date; message: string }[]>([]); 219 | const logsRef = useRef<{ time: Date; message: string }[]>([]); 220 | 221 | const clearLogs = () => { 222 | logsRef.current = []; 223 | setLogs([]); 224 | }; 225 | 226 | const logMessage = (...args: any[]) => { 227 | const time = new Date(); 228 | const message = args.join(" "); 229 | logsRef.current.push({ time, message }); 230 | console.log(`[${time.toLocaleTimeString()}] ${message}`); 231 | setLogs([...logsRef.current]); 232 | }; 233 | 234 | const logDisplay = () => { 235 | const logLines = logs.map((log) => ( 236 |

237 | [{log.time.toLocaleTimeString()}] {log.message} 238 |

239 | )); 240 | return ; 241 | }; 242 | return [logMessage, logDisplay] as const; 243 | }; 244 | 245 | class Streamer extends EventEmitter { 246 | ws: WebSocket; 247 | stream: MediaStream | null = null; 248 | processor: ScriptProcessorNode | null = null; 249 | vadMic: Promise | null = null; 250 | audioContext: AudioContext | null = null; 251 | userIsSpeaking: boolean = false; 252 | 253 | constructor(ws: WebSocket, private logMessage: (...args: any[]) => void) { 254 | super(); 255 | this.ws = ws; 256 | 257 | this.vadMic = vad.MicVAD.new({ 258 | onSpeechStart: () => { 259 | this.emit("speechStart"); 260 | logMessage("--- vad: speech start"); 261 | this.userIsSpeaking = true; 262 | }, 263 | onSpeechEnd: (audio) => { 264 | this.emit("speechEnd"); 265 | logMessage("--- vad: speech end"); 266 | ws.send(END_OF_SPEECH_TOKEN); 267 | this.userIsSpeaking = false; 268 | }, 269 | }); 270 | this.audioContext = new AudioContext(AudioContextSettings); 271 | } 272 | 273 | async startVoiceDetection() { 274 | (await this.vadMic!).start(); 275 | } 276 | 277 | async start(startVoiceDetection: boolean = false) { 278 | const constraints = { 279 | audio: true, 280 | }; 281 | navigator.mediaDevices.getUserMedia(constraints).then((stream) => { 282 | this.stream = stream; 283 | const audioContext = new AudioContext({ 284 | sampleRate: 24000, 285 | }); 286 | this.logMessage("audio context sample rate", audioContext.sampleRate); 287 | const source = audioContext.createMediaStreamSource(stream); 288 | this.logMessage("media stream source created"); 289 | this.processor = audioContext.createScriptProcessor(1024, 1, 1); 290 | this.processor.onaudioprocess = (event) => { 291 | if (this.ws.readyState === WebSocket.OPEN && this.userIsSpeaking) { 292 | this.ws.send(event.inputBuffer.getChannelData(0)); 293 | } 294 | }; 295 | source.connect(this.processor); 296 | this.processor.connect(audioContext.destination); 297 | }); 298 | if (startVoiceDetection) { 299 | await this.startVoiceDetection(); 300 | } 301 | } 302 | 303 | async stop(graceful: boolean = false) { 304 | this.audioContext?.suspend(); 305 | 306 | this.stream?.getTracks().forEach((track) => { 307 | track.stop(); 308 | this.stream?.removeTrack(track); 309 | }); 310 | this.processor && (this.processor.onaudioprocess = null); 311 | const vadMic = await this.vadMic; 312 | vadMic && vadMic.destroy(); 313 | this.vadMic = null; 314 | } 315 | } 316 | 317 | class Playback extends EventEmitter { 318 | samples: Float32Array[] = []; 319 | lastFramePlayed: "silence" | "non-silence" = "silence"; 320 | 321 | constructor(public audioContext: AudioContext) { 322 | super(); 323 | this.audioContext.suspend(); 324 | const scriptNode = this.audioContext.createScriptProcessor(1024, 1, 1); 325 | scriptNode.onaudioprocess = (event) => { 326 | if (this.samples.length > 0) { 327 | if (this.lastFramePlayed === "silence") { 328 | this.emit("playbackStart"); 329 | } 330 | this.lastFramePlayed = "non-silence"; 331 | event.outputBuffer.getChannelData(0).set(this.samples[0]); 332 | this.samples.shift(); 333 | } else { 334 | if (this.lastFramePlayed === "non-silence") { 335 | this.emit("playbackEnd"); 336 | } 337 | this.lastFramePlayed = "silence"; 338 | const silence = new Float32Array(1024); 339 | event.outputBuffer.getChannelData(0).set(silence); 340 | } 341 | }; 342 | 343 | const gainNode = this.audioContext.createGain(); 344 | gainNode.gain.value = 0.5; 345 | scriptNode.connect(gainNode); 346 | gainNode.connect(this.audioContext.destination); 347 | } 348 | 349 | async clear() { 350 | await this.audioContext.suspend(); 351 | const dirty = this.samples.length > 0; 352 | this.samples = []; 353 | await this.audioContext.resume(); 354 | this.emit("clear", { dirty }); 355 | this.lastFramePlayed = "silence"; 356 | return dirty; 357 | } 358 | 359 | start() { 360 | this.audioContext.resume(); 361 | } 362 | 363 | stop(graceful: boolean = false) { 364 | if (graceful) { 365 | if (this.samples.length > 0) { 366 | return setTimeout(() => { 367 | this.stop(true); 368 | }, 1000); 369 | } 370 | } else { 371 | this.audioContext.suspend(); 372 | } 373 | } 374 | 375 | addSamples(samples: Float32Array) { 376 | this.samples.push(samples); 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /web/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | margin: 0; 7 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 8 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 9 | sans-serif; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | background-color: #000; 13 | color: #fff; 14 | } 15 | 16 | code { 17 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 18 | monospace; 19 | } 20 | -------------------------------------------------------------------------------- /web/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { Analytics } from "@vercel/analytics/react"; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom/client'; 4 | import './index.css'; 5 | import App from './app'; 6 | import reportWebVitals from './reportWebVitals'; 7 | 8 | const root = ReactDOM.createRoot( 9 | document.getElementById('root') as HTMLElement 10 | ); 11 | 12 | root.render( 13 | 14 | 15 | 16 | 17 | ); 18 | 19 | // If you want to start measuring performance in your app, pass a function 20 | // to log results (for example: reportWebVitals(console.log)) 21 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 22 | reportWebVitals(); 23 | -------------------------------------------------------------------------------- /web/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /web/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /web/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./src/**/*.{js,jsx,ts,tsx}", 5 | ], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [], 10 | } 11 | 12 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------