├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.yml ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── api │ └── transcribe.js ├── components │ ├── animatedBars.js │ ├── animatedBars.module.css │ ├── arrow.js │ ├── dialog.js │ ├── dialog.module.css │ ├── iconbutton.js │ ├── iconbutton.module.css │ ├── message.js │ ├── message.module.css │ ├── microphone.js │ ├── microphoneOff.js │ ├── pause.js │ ├── play.js │ ├── progress.js │ ├── progress.module.css │ └── settings.js ├── index.js ├── index.module.css └── lib │ ├── upload.js │ ├── useStorage.js │ └── utils.js ├── public ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json ├── robots.txt ├── screenshot.png ├── screenshot2.png └── screenshot3.png ├── server.js └── styles └── app.css /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | .next 11 | .env 12 | .vscode 13 | 14 | node_modules 15 | dist 16 | dist-ssr 17 | *.local 18 | 19 | _bin 20 | *.bu.css 21 | *.bu.jsx 22 | *.bu.js 23 | *.m4a.srt 24 | *.m4a.txt 25 | *.m4a.vtt 26 | public/uploads/*.m4a 27 | public/uploads/*.m4a.srt 28 | public/uploads/*.m4a.txt 29 | public/uploads/*.m4a.vtt 30 | 31 | *.pem 32 | *.crt 33 | 34 | # Editor directories and files 35 | .vscode/* 36 | !.vscode/extensions.json 37 | .idea 38 | .DS_Store 39 | *.suo 40 | *.ntvs* 41 | *.njsproj 42 | *.sln 43 | *.sw? 44 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nvidia/cuda:11.8.0-base-ubuntu22.04 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | RUN apt update && apt -y install ffmpeg python3 python3-pip git 5 | RUN pip install git+https://github.com/openai/whisper.git 6 | RUN apt install curl -yq 7 | RUN curl -sL https://deb.nodesource.com/setup_19.x | bash - 8 | RUN apt install -yq nodejs 9 | WORKDIR /app 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022-present SuperShaneski 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | openai-whisper 2 | =========== 3 | 4 | This is a sample webapp implementation of [OpenAI Whisper](https://openai.com/blog/whisper/), an automatic speech recognition (ASR) system, using [Next.JS](https://nextjs.org/). 5 | 6 | It records audio data automatically and uploads the audio data to the server for transcribing/translating then sends back the result to the front end. 7 | It is also possible to playback the recorded audio to verify the output. 8 | 9 | > **Update:** If you want to use `Next 13` with experimental feature enabled (appDir), please check [openai-whisper-api](https://github.com/supershaneski/openai-whisper-api/) instead. Just set the flag to use whisper python module instead of whisper API. 10 | 11 | --- 12 | 13 | * Using `OpenAI` [Speech to Text API](https://platform.openai.com/docs/guides/speech-to-text), please check [openai-whisper-api](https://github.com/supershaneski/openai-whisper-api/) 14 | 15 | * If you are looking for voice-chat app using `Whisper`, please check [openai-whisper-talk](https://github.com/supershaneski/openai-whisper-talk/). 16 | 17 | * For `Nuxt.js` version, please check [openai-chatterbox](https://github.com/supershaneski/openai-chatterbox/). 18 | 19 | # Motivation 20 | 21 | It has been said that `Whisper` itself is [not designed to support ***real-time*** streaming tasks per se](https://github.com/openai/whisper/discussions/2) but it does not mean we cannot try, vain as it may be, lol. 22 | 23 | So this project is my attempt to make an ***almost real-time*** transcriber web application using openai `Whisper`. 24 | The efficacy of which depends on how fast the server can transcribe/translate the audio. 25 | 26 | I used `Next.js` so that I do not have to make separate backend and frontend apps. 27 | 28 | As for the backend, I used `exec` to execute shell command invoking `Whisper`. 29 | I have not yet find a way to `import` it as a `node.js` module. 30 | All examples with `import` seem to be using `python` server. 31 | 32 | ```javascript 33 | import { exec } from 'child_process' 34 | 35 | exec(`whisper './${filename}' --model tiny --language Japanese --task translate`, (err, stdout, stderr) => { 36 | if (err) { 37 | console.log(err) 38 | } else { 39 | console.log(stdout) 40 | console.log(stderr) 41 | } 42 | }) 43 | ``` 44 | 45 | Notice I am just using the `tiny` model to perform super fast transcribing task. 46 | This is all my system can handle otherwise it will come to a stand still. 47 | 48 | ## The App 49 | 50 | ![App](./public/screenshot.png "App") 51 | 52 | I changed the behavior of the app from previous version. 53 | Before, the app will record audio data continuously by some time interval, by default 5s. 54 | Right now, it will only start recording if it can detect sound. 55 | 56 | There is a threshold setting to eliminate background noise from triggering the audio capture. 57 | By default it is set to `-45dB` (0dB is the loudest sound). 58 | Adjust the variable `minDecibels` in `Settings` if you want to set it to lower or higher depending on your needs. 59 | 60 | In normal human conversation, it is said that we tend to pause, on average, around 2 seconds between each sentences. Keeping this in mind, if sound is not detected for more than 2 seconds, recording will stop and the audio data will be sent to the backend for transcribing. 61 | You can change this by editing the value of `maxPause`, by default set to `2500ms`. 62 | 63 | ![Output](./public/screenshot3.png "Output") 64 | 65 | It is possible to play the uploaded audio and follow the text output since the time period is shown. 66 | 67 | As for the code itself, I used `class component` (I know, I know...) because I had a difficult time to access `state variables` using hooks when I was developing. 68 | 69 | ![Settings](./public/screenshot2.png "Settings") 70 | 71 | Aside from `minDecibels` and `maxPause`, you can also change several `Whisper` options such as `language`, `model` and `task` from the `Settings` dialog. Please check [Whisper's github repository](https://github.com/openai/whisper) for the explanation on the options. 72 | 73 | There are still lots of things to do so this project is still a work in progress... 74 | 75 | # Setup 76 | 77 | First, you need to install [`Whisper`](https://github.com/openai/whisper) and its `Python` dependencies 78 | 79 | ```sh 80 | $ pip install git+https://github.com/openai/whisper.git 81 | ``` 82 | 83 | You also need `ffmpeg` installed on your system 84 | 85 | ```sh 86 | # macos 87 | $ brew install ffmpeg 88 | 89 | # windows using chocolatey 90 | $ choco install ffmpeg 91 | 92 | # windows using scoop 93 | $ scoop install ffmpeg 94 | ``` 95 | 96 | By this time, you can test `Whisper` using command line 97 | 98 | ```sh 99 | $ whisper myaudiofile.ogg --language Japanese --task translate 100 | ``` 101 | 102 | If that is successful, you can proceed to install this app. 103 | 104 | Clone the repository and install the dependencies 105 | 106 | ```sh 107 | $ git clone https://github.com/supershaneski/openai-whisper.git myproject 108 | 109 | $ cd myproject 110 | 111 | $ npm install 112 | 113 | $ npm run dev 114 | ``` 115 | 116 | Open your browser to `http://localhost:3006/` to load the application page. 117 | 118 | ## Using HTTPS 119 | 120 | You might want to run this app using `https` protocol. 121 | This is needed if you want to use a separate device for audio capture and use your machine as server. 122 | 123 | In order to do so, prepare the proper `certificate` and `key` files and edit `server.js` at the root directory. 124 | 125 | Then run 126 | 127 | ```sh 128 | $ node server.js 129 | ``` 130 | 131 | Now, open your browser to `https://localhost:3006/` to load the page. 132 | 133 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | node: 4 | build: . 5 | ports: 6 | - '3006:3006' 7 | volumes: 8 | - .:/app/. 9 | runtime: nvidia 10 | command: '/bin/sh -c "npm install && node server.js"' 11 | deploy: 12 | resources: 13 | reservations: 14 | devices: 15 | - driver: nvidia 16 | count: 1 17 | capabilities: [gpu] 18 | 19 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const securityHeaders = [ 2 | { 3 | key: 'Strict-Transport-Security', 4 | value: 'max-age=63072000; includeSubDomains; preload' 5 | }, 6 | ] 7 | 8 | module.exports = { 9 | webpack: function(config) { 10 | config.resolve.fallback = { fs: false } 11 | config.module.rules.push({ 12 | test: /\.md$/, 13 | use: 'raw-loader', 14 | }) 15 | return config 16 | }, 17 | env: { 18 | siteTitle: 'openai Whisper - Sample WebApp', 19 | }, 20 | async headers() { 21 | return [ 22 | { 23 | source: '/:path*', 24 | headers: securityHeaders, 25 | } 26 | ] 27 | }, 28 | serverRuntimeConfig: { 29 | PROJECT_ROOT: __dirname, 30 | }, 31 | trailingSlash: true, 32 | exportPathMap: async function( 33 | defaultPathMap, 34 | { dev, dir, outDir, distDir, buildId } 35 | ) { 36 | return { 37 | '/': { page: '/' }, 38 | }; 39 | } 40 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openai-whisper", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "openai-whisper", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "multer": "^1.4.5-lts.1", 13 | "next": "^13.0.1", 14 | "next-connect": "^0.13.0", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0" 17 | } 18 | }, 19 | "node_modules/@next/env": { 20 | "version": "13.0.1", 21 | "resolved": "https://registry.npmjs.org/@next/env/-/env-13.0.1.tgz", 22 | "integrity": "sha512-gK60YoFae3s8qi5UgIzbvxOhsh5gKyEaiKH5+kLBUYXLlrPyWJR2xKBj2WqvHkO7wDX7/Hed3DAqjSpU4ijIvQ==" 23 | }, 24 | "node_modules/@next/swc-android-arm-eabi": { 25 | "version": "13.0.1", 26 | "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.0.1.tgz", 27 | "integrity": "sha512-M28QSbohZlNXNn//HY6lV2T3YaMzG58Jwr0YwOdVmOQv6i+7lu6xe3GqQu4kdqInqhLrBXnL+nabFuGTVSHtTg==", 28 | "cpu": [ 29 | "arm" 30 | ], 31 | "optional": true, 32 | "os": [ 33 | "android" 34 | ], 35 | "engines": { 36 | "node": ">= 10" 37 | } 38 | }, 39 | "node_modules/@next/swc-android-arm64": { 40 | "version": "13.0.1", 41 | "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-13.0.1.tgz", 42 | "integrity": "sha512-szmO/i6GoHcPXcbhUKhwBMETWHNXH3ITz9wfxwOOFBNKdDU8pjKsHL88lg28aOiQYZSU1sxu1v1p9KY5kJIZCg==", 43 | "cpu": [ 44 | "arm64" 45 | ], 46 | "optional": true, 47 | "os": [ 48 | "android" 49 | ], 50 | "engines": { 51 | "node": ">= 10" 52 | } 53 | }, 54 | "node_modules/@next/swc-darwin-arm64": { 55 | "version": "13.0.1", 56 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.0.1.tgz", 57 | "integrity": "sha512-O1RxCaiDNOjGZmdAp6SQoHUITt9aVDQXoR3lZ/TloI/NKRAyAV4u0KUUofK+KaZeHOmVTnPUaQuCyZSc3i1x5Q==", 58 | "cpu": [ 59 | "arm64" 60 | ], 61 | "optional": true, 62 | "os": [ 63 | "darwin" 64 | ], 65 | "engines": { 66 | "node": ">= 10" 67 | } 68 | }, 69 | "node_modules/@next/swc-darwin-x64": { 70 | "version": "13.0.1", 71 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.0.1.tgz", 72 | "integrity": "sha512-8E6BY/VO+QqQkthhoWgB8mJMw1NcN9Vhl2OwEwxv8jy2r3zjeU+WNRxz4y8RLbcY0R1h+vHlXuP0mLnuac84tQ==", 73 | "cpu": [ 74 | "x64" 75 | ], 76 | "optional": true, 77 | "os": [ 78 | "darwin" 79 | ], 80 | "engines": { 81 | "node": ">= 10" 82 | } 83 | }, 84 | "node_modules/@next/swc-freebsd-x64": { 85 | "version": "13.0.1", 86 | "resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.0.1.tgz", 87 | "integrity": "sha512-ocwoOxm2KVwF50RyoAT+2RQPLlkyoF7sAqzMUVgj+S6+DTkY3iwH+Zpo0XAk2pnqT9qguOrKnEpq9EIx//+K7Q==", 88 | "cpu": [ 89 | "x64" 90 | ], 91 | "optional": true, 92 | "os": [ 93 | "freebsd" 94 | ], 95 | "engines": { 96 | "node": ">= 10" 97 | } 98 | }, 99 | "node_modules/@next/swc-linux-arm-gnueabihf": { 100 | "version": "13.0.1", 101 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.0.1.tgz", 102 | "integrity": "sha512-yO7e3zITfGol/N6lPQnmIRi0WyuILBMXrvH6EdmWzzqMDJFfTCII6l+B6gMO5WVDCTQUGQlQRNZ7sFqWR4I71g==", 103 | "cpu": [ 104 | "arm" 105 | ], 106 | "optional": true, 107 | "os": [ 108 | "linux" 109 | ], 110 | "engines": { 111 | "node": ">= 10" 112 | } 113 | }, 114 | "node_modules/@next/swc-linux-arm64-gnu": { 115 | "version": "13.0.1", 116 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.0.1.tgz", 117 | "integrity": "sha512-OEs6WDPDI8RyM8SjOqTDMqMBfOlU97VnW6ZMXUvzUTyH0K9c7NF+cn7UMu+I4tKFN0uJ9WQs/6TYaFBGkgoVVA==", 118 | "cpu": [ 119 | "arm64" 120 | ], 121 | "optional": true, 122 | "os": [ 123 | "linux" 124 | ], 125 | "engines": { 126 | "node": ">= 10" 127 | } 128 | }, 129 | "node_modules/@next/swc-linux-arm64-musl": { 130 | "version": "13.0.1", 131 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.0.1.tgz", 132 | "integrity": "sha512-y5ypFK0Y3urZSFoQxbtDqvKsBx026sz+Fm+xHlPWlGHNZrbs3Q812iONjcZTo09QwRMk5X86iMWBRxV18xMhaw==", 133 | "cpu": [ 134 | "arm64" 135 | ], 136 | "optional": true, 137 | "os": [ 138 | "linux" 139 | ], 140 | "engines": { 141 | "node": ">= 10" 142 | } 143 | }, 144 | "node_modules/@next/swc-linux-x64-gnu": { 145 | "version": "13.0.1", 146 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.0.1.tgz", 147 | "integrity": "sha512-XDIHEE6SU8VCF+dUVntD6PDv6RK31N0forx9kucZBYirbe8vCZ+Yx8hYgvtIaGrTcWtGxibxmND0pIuHDq8H5g==", 148 | "cpu": [ 149 | "x64" 150 | ], 151 | "optional": true, 152 | "os": [ 153 | "linux" 154 | ], 155 | "engines": { 156 | "node": ">= 10" 157 | } 158 | }, 159 | "node_modules/@next/swc-linux-x64-musl": { 160 | "version": "13.0.1", 161 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.0.1.tgz", 162 | "integrity": "sha512-yxIOuuz5EOx0F1FDtsyzaLgnDym0Ysxv8CWeJyDTKKmt9BVyITg6q/cD+RP9bEkT1TQi+PYXIMATSz675Q82xw==", 163 | "cpu": [ 164 | "x64" 165 | ], 166 | "optional": true, 167 | "os": [ 168 | "linux" 169 | ], 170 | "engines": { 171 | "node": ">= 10" 172 | } 173 | }, 174 | "node_modules/@next/swc-win32-arm64-msvc": { 175 | "version": "13.0.1", 176 | "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.0.1.tgz", 177 | "integrity": "sha512-+ucLe2qgQzP+FM94jD4ns6LDGyMFaX9k3lVHqu/tsQCy2giMymbport4y4p77mYcXEMlDaHMzlHgOQyHRniWFA==", 178 | "cpu": [ 179 | "arm64" 180 | ], 181 | "optional": true, 182 | "os": [ 183 | "win32" 184 | ], 185 | "engines": { 186 | "node": ">= 10" 187 | } 188 | }, 189 | "node_modules/@next/swc-win32-ia32-msvc": { 190 | "version": "13.0.1", 191 | "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.0.1.tgz", 192 | "integrity": "sha512-Krr/qGN7OB35oZuvMAZKoXDt2IapynIWLh5A5rz6AODb7f/ZJqyAuZSK12vOa2zKdobS36Qm4IlxxBqn9c00MA==", 193 | "cpu": [ 194 | "ia32" 195 | ], 196 | "optional": true, 197 | "os": [ 198 | "win32" 199 | ], 200 | "engines": { 201 | "node": ">= 10" 202 | } 203 | }, 204 | "node_modules/@next/swc-win32-x64-msvc": { 205 | "version": "13.0.1", 206 | "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.0.1.tgz", 207 | "integrity": "sha512-t/0G33t/6VGWZUGCOT7rG42qqvf/x+MrFp1CU+8CN6PrjSSL57R5bqkXfubV9t4eCEnUxVP+5Hn3MoEXEebtEw==", 208 | "cpu": [ 209 | "x64" 210 | ], 211 | "optional": true, 212 | "os": [ 213 | "win32" 214 | ], 215 | "engines": { 216 | "node": ">= 10" 217 | } 218 | }, 219 | "node_modules/@swc/helpers": { 220 | "version": "0.4.11", 221 | "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.11.tgz", 222 | "integrity": "sha512-rEUrBSGIoSFuYxwBYtlUFMlE2CwGhmW+w9355/5oduSw8e5h2+Tj4UrAGNNgP9915++wj5vkQo0UuOBqOAq4nw==", 223 | "dependencies": { 224 | "tslib": "^2.4.0" 225 | } 226 | }, 227 | "node_modules/append-field": { 228 | "version": "1.0.0", 229 | "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", 230 | "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" 231 | }, 232 | "node_modules/buffer-from": { 233 | "version": "1.1.2", 234 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", 235 | "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" 236 | }, 237 | "node_modules/busboy": { 238 | "version": "1.6.0", 239 | "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", 240 | "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", 241 | "dependencies": { 242 | "streamsearch": "^1.1.0" 243 | }, 244 | "engines": { 245 | "node": ">=10.16.0" 246 | } 247 | }, 248 | "node_modules/caniuse-lite": { 249 | "version": "1.0.30001427", 250 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001427.tgz", 251 | "integrity": "sha512-lfXQ73oB9c8DP5Suxaszm+Ta2sr/4tf8+381GkIm1MLj/YdLf+rEDyDSRCzeltuyTVGm+/s18gdZ0q+Wmp8VsQ==", 252 | "funding": [ 253 | { 254 | "type": "opencollective", 255 | "url": "https://opencollective.com/browserslist" 256 | }, 257 | { 258 | "type": "tidelift", 259 | "url": "https://tidelift.com/funding/github/npm/caniuse-lite" 260 | } 261 | ] 262 | }, 263 | "node_modules/client-only": { 264 | "version": "0.0.1", 265 | "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", 266 | "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" 267 | }, 268 | "node_modules/concat-stream": { 269 | "version": "1.6.2", 270 | "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", 271 | "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", 272 | "engines": [ 273 | "node >= 0.8" 274 | ], 275 | "dependencies": { 276 | "buffer-from": "^1.0.0", 277 | "inherits": "^2.0.3", 278 | "readable-stream": "^2.2.2", 279 | "typedarray": "^0.0.6" 280 | } 281 | }, 282 | "node_modules/core-util-is": { 283 | "version": "1.0.3", 284 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", 285 | "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" 286 | }, 287 | "node_modules/inherits": { 288 | "version": "2.0.4", 289 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 290 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 291 | }, 292 | "node_modules/isarray": { 293 | "version": "1.0.0", 294 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 295 | "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" 296 | }, 297 | "node_modules/js-tokens": { 298 | "version": "4.0.0", 299 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 300 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 301 | }, 302 | "node_modules/loose-envify": { 303 | "version": "1.4.0", 304 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 305 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 306 | "dependencies": { 307 | "js-tokens": "^3.0.0 || ^4.0.0" 308 | }, 309 | "bin": { 310 | "loose-envify": "cli.js" 311 | } 312 | }, 313 | "node_modules/media-typer": { 314 | "version": "0.3.0", 315 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 316 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 317 | "engines": { 318 | "node": ">= 0.6" 319 | } 320 | }, 321 | "node_modules/mime-db": { 322 | "version": "1.52.0", 323 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 324 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 325 | "engines": { 326 | "node": ">= 0.6" 327 | } 328 | }, 329 | "node_modules/mime-types": { 330 | "version": "2.1.35", 331 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 332 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 333 | "dependencies": { 334 | "mime-db": "1.52.0" 335 | }, 336 | "engines": { 337 | "node": ">= 0.6" 338 | } 339 | }, 340 | "node_modules/minimist": { 341 | "version": "1.2.7", 342 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", 343 | "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", 344 | "funding": { 345 | "url": "https://github.com/sponsors/ljharb" 346 | } 347 | }, 348 | "node_modules/mkdirp": { 349 | "version": "0.5.6", 350 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", 351 | "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", 352 | "dependencies": { 353 | "minimist": "^1.2.6" 354 | }, 355 | "bin": { 356 | "mkdirp": "bin/cmd.js" 357 | } 358 | }, 359 | "node_modules/multer": { 360 | "version": "1.4.5-lts.1", 361 | "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", 362 | "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", 363 | "dependencies": { 364 | "append-field": "^1.0.0", 365 | "busboy": "^1.0.0", 366 | "concat-stream": "^1.5.2", 367 | "mkdirp": "^0.5.4", 368 | "object-assign": "^4.1.1", 369 | "type-is": "^1.6.4", 370 | "xtend": "^4.0.0" 371 | }, 372 | "engines": { 373 | "node": ">= 6.0.0" 374 | } 375 | }, 376 | "node_modules/nanoid": { 377 | "version": "3.3.4", 378 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", 379 | "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", 380 | "bin": { 381 | "nanoid": "bin/nanoid.cjs" 382 | }, 383 | "engines": { 384 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 385 | } 386 | }, 387 | "node_modules/next": { 388 | "version": "13.0.1", 389 | "resolved": "https://registry.npmjs.org/next/-/next-13.0.1.tgz", 390 | "integrity": "sha512-ErCNBPIeZMKFn6hX+ZBSlqZVgJIeitEqhGTuQUNmYXJ07/A71DZ7AJI8eyHYUdBb686LUpV1/oBdTq9RpzRVPg==", 391 | "dependencies": { 392 | "@next/env": "13.0.1", 393 | "@swc/helpers": "0.4.11", 394 | "caniuse-lite": "^1.0.30001406", 395 | "postcss": "8.4.14", 396 | "styled-jsx": "5.1.0", 397 | "use-sync-external-store": "1.2.0" 398 | }, 399 | "bin": { 400 | "next": "dist/bin/next" 401 | }, 402 | "engines": { 403 | "node": ">=14.6.0" 404 | }, 405 | "optionalDependencies": { 406 | "@next/swc-android-arm-eabi": "13.0.1", 407 | "@next/swc-android-arm64": "13.0.1", 408 | "@next/swc-darwin-arm64": "13.0.1", 409 | "@next/swc-darwin-x64": "13.0.1", 410 | "@next/swc-freebsd-x64": "13.0.1", 411 | "@next/swc-linux-arm-gnueabihf": "13.0.1", 412 | "@next/swc-linux-arm64-gnu": "13.0.1", 413 | "@next/swc-linux-arm64-musl": "13.0.1", 414 | "@next/swc-linux-x64-gnu": "13.0.1", 415 | "@next/swc-linux-x64-musl": "13.0.1", 416 | "@next/swc-win32-arm64-msvc": "13.0.1", 417 | "@next/swc-win32-ia32-msvc": "13.0.1", 418 | "@next/swc-win32-x64-msvc": "13.0.1" 419 | }, 420 | "peerDependencies": { 421 | "fibers": ">= 3.1.0", 422 | "node-sass": "^6.0.0 || ^7.0.0", 423 | "react": "^18.2.0", 424 | "react-dom": "^18.2.0", 425 | "sass": "^1.3.0" 426 | }, 427 | "peerDependenciesMeta": { 428 | "fibers": { 429 | "optional": true 430 | }, 431 | "node-sass": { 432 | "optional": true 433 | }, 434 | "sass": { 435 | "optional": true 436 | } 437 | } 438 | }, 439 | "node_modules/next-connect": { 440 | "version": "0.13.0", 441 | "resolved": "https://registry.npmjs.org/next-connect/-/next-connect-0.13.0.tgz", 442 | "integrity": "sha512-f2G4edY01XomjCECSrgOpb/zzQinJO6Whd8Zds0+rLUYhj5cLwkh6FVvZsQCSSbxSc4k9nCwNuk5NLIhvO1gUA==", 443 | "dependencies": { 444 | "trouter": "^3.2.0" 445 | } 446 | }, 447 | "node_modules/object-assign": { 448 | "version": "4.1.1", 449 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 450 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 451 | "engines": { 452 | "node": ">=0.10.0" 453 | } 454 | }, 455 | "node_modules/picocolors": { 456 | "version": "1.0.0", 457 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", 458 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" 459 | }, 460 | "node_modules/postcss": { 461 | "version": "8.4.14", 462 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", 463 | "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", 464 | "funding": [ 465 | { 466 | "type": "opencollective", 467 | "url": "https://opencollective.com/postcss/" 468 | }, 469 | { 470 | "type": "tidelift", 471 | "url": "https://tidelift.com/funding/github/npm/postcss" 472 | } 473 | ], 474 | "dependencies": { 475 | "nanoid": "^3.3.4", 476 | "picocolors": "^1.0.0", 477 | "source-map-js": "^1.0.2" 478 | }, 479 | "engines": { 480 | "node": "^10 || ^12 || >=14" 481 | } 482 | }, 483 | "node_modules/process-nextick-args": { 484 | "version": "2.0.1", 485 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 486 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" 487 | }, 488 | "node_modules/react": { 489 | "version": "18.2.0", 490 | "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", 491 | "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", 492 | "dependencies": { 493 | "loose-envify": "^1.1.0" 494 | }, 495 | "engines": { 496 | "node": ">=0.10.0" 497 | } 498 | }, 499 | "node_modules/react-dom": { 500 | "version": "18.2.0", 501 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", 502 | "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", 503 | "dependencies": { 504 | "loose-envify": "^1.1.0", 505 | "scheduler": "^0.23.0" 506 | }, 507 | "peerDependencies": { 508 | "react": "^18.2.0" 509 | } 510 | }, 511 | "node_modules/readable-stream": { 512 | "version": "2.3.7", 513 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", 514 | "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", 515 | "dependencies": { 516 | "core-util-is": "~1.0.0", 517 | "inherits": "~2.0.3", 518 | "isarray": "~1.0.0", 519 | "process-nextick-args": "~2.0.0", 520 | "safe-buffer": "~5.1.1", 521 | "string_decoder": "~1.1.1", 522 | "util-deprecate": "~1.0.1" 523 | } 524 | }, 525 | "node_modules/regexparam": { 526 | "version": "1.3.0", 527 | "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-1.3.0.tgz", 528 | "integrity": "sha512-6IQpFBv6e5vz1QAqI+V4k8P2e/3gRrqfCJ9FI+O1FLQTO+Uz6RXZEZOPmTJ6hlGj7gkERzY5BRCv09whKP96/g==", 529 | "engines": { 530 | "node": ">=6" 531 | } 532 | }, 533 | "node_modules/safe-buffer": { 534 | "version": "5.1.2", 535 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 536 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 537 | }, 538 | "node_modules/scheduler": { 539 | "version": "0.23.0", 540 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", 541 | "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", 542 | "dependencies": { 543 | "loose-envify": "^1.1.0" 544 | } 545 | }, 546 | "node_modules/source-map-js": { 547 | "version": "1.0.2", 548 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", 549 | "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", 550 | "engines": { 551 | "node": ">=0.10.0" 552 | } 553 | }, 554 | "node_modules/streamsearch": { 555 | "version": "1.1.0", 556 | "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", 557 | "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", 558 | "engines": { 559 | "node": ">=10.0.0" 560 | } 561 | }, 562 | "node_modules/string_decoder": { 563 | "version": "1.1.1", 564 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 565 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 566 | "dependencies": { 567 | "safe-buffer": "~5.1.0" 568 | } 569 | }, 570 | "node_modules/styled-jsx": { 571 | "version": "5.1.0", 572 | "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.0.tgz", 573 | "integrity": "sha512-/iHaRJt9U7T+5tp6TRelLnqBqiaIT0HsO0+vgyj8hK2KUk7aejFqRrumqPUlAqDwAj8IbS/1hk3IhBAAK/FCUQ==", 574 | "dependencies": { 575 | "client-only": "0.0.1" 576 | }, 577 | "engines": { 578 | "node": ">= 12.0.0" 579 | }, 580 | "peerDependencies": { 581 | "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" 582 | }, 583 | "peerDependenciesMeta": { 584 | "@babel/core": { 585 | "optional": true 586 | }, 587 | "babel-plugin-macros": { 588 | "optional": true 589 | } 590 | } 591 | }, 592 | "node_modules/trouter": { 593 | "version": "3.2.0", 594 | "resolved": "https://registry.npmjs.org/trouter/-/trouter-3.2.0.tgz", 595 | "integrity": "sha512-rLLXbhTObLy2MBVjLC+jTnoIKw99n0GuJs9ov10J870vDw5qhTurPzsDrudNtBf5w/CZ9ctZy2p2IMmhGcel2w==", 596 | "dependencies": { 597 | "regexparam": "^1.3.0" 598 | }, 599 | "engines": { 600 | "node": ">=6" 601 | } 602 | }, 603 | "node_modules/tslib": { 604 | "version": "2.4.1", 605 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", 606 | "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" 607 | }, 608 | "node_modules/type-is": { 609 | "version": "1.6.18", 610 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 611 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 612 | "dependencies": { 613 | "media-typer": "0.3.0", 614 | "mime-types": "~2.1.24" 615 | }, 616 | "engines": { 617 | "node": ">= 0.6" 618 | } 619 | }, 620 | "node_modules/typedarray": { 621 | "version": "0.0.6", 622 | "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", 623 | "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" 624 | }, 625 | "node_modules/use-sync-external-store": { 626 | "version": "1.2.0", 627 | "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", 628 | "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", 629 | "peerDependencies": { 630 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0" 631 | } 632 | }, 633 | "node_modules/util-deprecate": { 634 | "version": "1.0.2", 635 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 636 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" 637 | }, 638 | "node_modules/xtend": { 639 | "version": "4.0.2", 640 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 641 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", 642 | "engines": { 643 | "node": ">=0.4" 644 | } 645 | } 646 | }, 647 | "dependencies": { 648 | "@next/env": { 649 | "version": "13.0.1", 650 | "resolved": "https://registry.npmjs.org/@next/env/-/env-13.0.1.tgz", 651 | "integrity": "sha512-gK60YoFae3s8qi5UgIzbvxOhsh5gKyEaiKH5+kLBUYXLlrPyWJR2xKBj2WqvHkO7wDX7/Hed3DAqjSpU4ijIvQ==" 652 | }, 653 | "@next/swc-android-arm-eabi": { 654 | "version": "13.0.1", 655 | "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.0.1.tgz", 656 | "integrity": "sha512-M28QSbohZlNXNn//HY6lV2T3YaMzG58Jwr0YwOdVmOQv6i+7lu6xe3GqQu4kdqInqhLrBXnL+nabFuGTVSHtTg==", 657 | "optional": true 658 | }, 659 | "@next/swc-android-arm64": { 660 | "version": "13.0.1", 661 | "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-13.0.1.tgz", 662 | "integrity": "sha512-szmO/i6GoHcPXcbhUKhwBMETWHNXH3ITz9wfxwOOFBNKdDU8pjKsHL88lg28aOiQYZSU1sxu1v1p9KY5kJIZCg==", 663 | "optional": true 664 | }, 665 | "@next/swc-darwin-arm64": { 666 | "version": "13.0.1", 667 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.0.1.tgz", 668 | "integrity": "sha512-O1RxCaiDNOjGZmdAp6SQoHUITt9aVDQXoR3lZ/TloI/NKRAyAV4u0KUUofK+KaZeHOmVTnPUaQuCyZSc3i1x5Q==", 669 | "optional": true 670 | }, 671 | "@next/swc-darwin-x64": { 672 | "version": "13.0.1", 673 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.0.1.tgz", 674 | "integrity": "sha512-8E6BY/VO+QqQkthhoWgB8mJMw1NcN9Vhl2OwEwxv8jy2r3zjeU+WNRxz4y8RLbcY0R1h+vHlXuP0mLnuac84tQ==", 675 | "optional": true 676 | }, 677 | "@next/swc-freebsd-x64": { 678 | "version": "13.0.1", 679 | "resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.0.1.tgz", 680 | "integrity": "sha512-ocwoOxm2KVwF50RyoAT+2RQPLlkyoF7sAqzMUVgj+S6+DTkY3iwH+Zpo0XAk2pnqT9qguOrKnEpq9EIx//+K7Q==", 681 | "optional": true 682 | }, 683 | "@next/swc-linux-arm-gnueabihf": { 684 | "version": "13.0.1", 685 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.0.1.tgz", 686 | "integrity": "sha512-yO7e3zITfGol/N6lPQnmIRi0WyuILBMXrvH6EdmWzzqMDJFfTCII6l+B6gMO5WVDCTQUGQlQRNZ7sFqWR4I71g==", 687 | "optional": true 688 | }, 689 | "@next/swc-linux-arm64-gnu": { 690 | "version": "13.0.1", 691 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.0.1.tgz", 692 | "integrity": "sha512-OEs6WDPDI8RyM8SjOqTDMqMBfOlU97VnW6ZMXUvzUTyH0K9c7NF+cn7UMu+I4tKFN0uJ9WQs/6TYaFBGkgoVVA==", 693 | "optional": true 694 | }, 695 | "@next/swc-linux-arm64-musl": { 696 | "version": "13.0.1", 697 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.0.1.tgz", 698 | "integrity": "sha512-y5ypFK0Y3urZSFoQxbtDqvKsBx026sz+Fm+xHlPWlGHNZrbs3Q812iONjcZTo09QwRMk5X86iMWBRxV18xMhaw==", 699 | "optional": true 700 | }, 701 | "@next/swc-linux-x64-gnu": { 702 | "version": "13.0.1", 703 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.0.1.tgz", 704 | "integrity": "sha512-XDIHEE6SU8VCF+dUVntD6PDv6RK31N0forx9kucZBYirbe8vCZ+Yx8hYgvtIaGrTcWtGxibxmND0pIuHDq8H5g==", 705 | "optional": true 706 | }, 707 | "@next/swc-linux-x64-musl": { 708 | "version": "13.0.1", 709 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.0.1.tgz", 710 | "integrity": "sha512-yxIOuuz5EOx0F1FDtsyzaLgnDym0Ysxv8CWeJyDTKKmt9BVyITg6q/cD+RP9bEkT1TQi+PYXIMATSz675Q82xw==", 711 | "optional": true 712 | }, 713 | "@next/swc-win32-arm64-msvc": { 714 | "version": "13.0.1", 715 | "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.0.1.tgz", 716 | "integrity": "sha512-+ucLe2qgQzP+FM94jD4ns6LDGyMFaX9k3lVHqu/tsQCy2giMymbport4y4p77mYcXEMlDaHMzlHgOQyHRniWFA==", 717 | "optional": true 718 | }, 719 | "@next/swc-win32-ia32-msvc": { 720 | "version": "13.0.1", 721 | "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.0.1.tgz", 722 | "integrity": "sha512-Krr/qGN7OB35oZuvMAZKoXDt2IapynIWLh5A5rz6AODb7f/ZJqyAuZSK12vOa2zKdobS36Qm4IlxxBqn9c00MA==", 723 | "optional": true 724 | }, 725 | "@next/swc-win32-x64-msvc": { 726 | "version": "13.0.1", 727 | "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.0.1.tgz", 728 | "integrity": "sha512-t/0G33t/6VGWZUGCOT7rG42qqvf/x+MrFp1CU+8CN6PrjSSL57R5bqkXfubV9t4eCEnUxVP+5Hn3MoEXEebtEw==", 729 | "optional": true 730 | }, 731 | "@swc/helpers": { 732 | "version": "0.4.11", 733 | "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.11.tgz", 734 | "integrity": "sha512-rEUrBSGIoSFuYxwBYtlUFMlE2CwGhmW+w9355/5oduSw8e5h2+Tj4UrAGNNgP9915++wj5vkQo0UuOBqOAq4nw==", 735 | "requires": { 736 | "tslib": "^2.4.0" 737 | } 738 | }, 739 | "append-field": { 740 | "version": "1.0.0", 741 | "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", 742 | "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" 743 | }, 744 | "buffer-from": { 745 | "version": "1.1.2", 746 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", 747 | "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" 748 | }, 749 | "busboy": { 750 | "version": "1.6.0", 751 | "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", 752 | "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", 753 | "requires": { 754 | "streamsearch": "^1.1.0" 755 | } 756 | }, 757 | "caniuse-lite": { 758 | "version": "1.0.30001427", 759 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001427.tgz", 760 | "integrity": "sha512-lfXQ73oB9c8DP5Suxaszm+Ta2sr/4tf8+381GkIm1MLj/YdLf+rEDyDSRCzeltuyTVGm+/s18gdZ0q+Wmp8VsQ==" 761 | }, 762 | "client-only": { 763 | "version": "0.0.1", 764 | "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", 765 | "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" 766 | }, 767 | "concat-stream": { 768 | "version": "1.6.2", 769 | "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", 770 | "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", 771 | "requires": { 772 | "buffer-from": "^1.0.0", 773 | "inherits": "^2.0.3", 774 | "readable-stream": "^2.2.2", 775 | "typedarray": "^0.0.6" 776 | } 777 | }, 778 | "core-util-is": { 779 | "version": "1.0.3", 780 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", 781 | "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" 782 | }, 783 | "inherits": { 784 | "version": "2.0.4", 785 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 786 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 787 | }, 788 | "isarray": { 789 | "version": "1.0.0", 790 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 791 | "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" 792 | }, 793 | "js-tokens": { 794 | "version": "4.0.0", 795 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 796 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 797 | }, 798 | "loose-envify": { 799 | "version": "1.4.0", 800 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 801 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 802 | "requires": { 803 | "js-tokens": "^3.0.0 || ^4.0.0" 804 | } 805 | }, 806 | "media-typer": { 807 | "version": "0.3.0", 808 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 809 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" 810 | }, 811 | "mime-db": { 812 | "version": "1.52.0", 813 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 814 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" 815 | }, 816 | "mime-types": { 817 | "version": "2.1.35", 818 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 819 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 820 | "requires": { 821 | "mime-db": "1.52.0" 822 | } 823 | }, 824 | "minimist": { 825 | "version": "1.2.7", 826 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", 827 | "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" 828 | }, 829 | "mkdirp": { 830 | "version": "0.5.6", 831 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", 832 | "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", 833 | "requires": { 834 | "minimist": "^1.2.6" 835 | } 836 | }, 837 | "multer": { 838 | "version": "1.4.5-lts.1", 839 | "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", 840 | "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", 841 | "requires": { 842 | "append-field": "^1.0.0", 843 | "busboy": "^1.0.0", 844 | "concat-stream": "^1.5.2", 845 | "mkdirp": "^0.5.4", 846 | "object-assign": "^4.1.1", 847 | "type-is": "^1.6.4", 848 | "xtend": "^4.0.0" 849 | } 850 | }, 851 | "nanoid": { 852 | "version": "3.3.4", 853 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", 854 | "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" 855 | }, 856 | "next": { 857 | "version": "13.0.1", 858 | "resolved": "https://registry.npmjs.org/next/-/next-13.0.1.tgz", 859 | "integrity": "sha512-ErCNBPIeZMKFn6hX+ZBSlqZVgJIeitEqhGTuQUNmYXJ07/A71DZ7AJI8eyHYUdBb686LUpV1/oBdTq9RpzRVPg==", 860 | "requires": { 861 | "@next/env": "13.0.1", 862 | "@next/swc-android-arm-eabi": "13.0.1", 863 | "@next/swc-android-arm64": "13.0.1", 864 | "@next/swc-darwin-arm64": "13.0.1", 865 | "@next/swc-darwin-x64": "13.0.1", 866 | "@next/swc-freebsd-x64": "13.0.1", 867 | "@next/swc-linux-arm-gnueabihf": "13.0.1", 868 | "@next/swc-linux-arm64-gnu": "13.0.1", 869 | "@next/swc-linux-arm64-musl": "13.0.1", 870 | "@next/swc-linux-x64-gnu": "13.0.1", 871 | "@next/swc-linux-x64-musl": "13.0.1", 872 | "@next/swc-win32-arm64-msvc": "13.0.1", 873 | "@next/swc-win32-ia32-msvc": "13.0.1", 874 | "@next/swc-win32-x64-msvc": "13.0.1", 875 | "@swc/helpers": "0.4.11", 876 | "caniuse-lite": "^1.0.30001406", 877 | "postcss": "8.4.14", 878 | "styled-jsx": "5.1.0", 879 | "use-sync-external-store": "1.2.0" 880 | } 881 | }, 882 | "next-connect": { 883 | "version": "0.13.0", 884 | "resolved": "https://registry.npmjs.org/next-connect/-/next-connect-0.13.0.tgz", 885 | "integrity": "sha512-f2G4edY01XomjCECSrgOpb/zzQinJO6Whd8Zds0+rLUYhj5cLwkh6FVvZsQCSSbxSc4k9nCwNuk5NLIhvO1gUA==", 886 | "requires": { 887 | "trouter": "^3.2.0" 888 | } 889 | }, 890 | "object-assign": { 891 | "version": "4.1.1", 892 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 893 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" 894 | }, 895 | "picocolors": { 896 | "version": "1.0.0", 897 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", 898 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" 899 | }, 900 | "postcss": { 901 | "version": "8.4.14", 902 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", 903 | "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", 904 | "requires": { 905 | "nanoid": "^3.3.4", 906 | "picocolors": "^1.0.0", 907 | "source-map-js": "^1.0.2" 908 | } 909 | }, 910 | "process-nextick-args": { 911 | "version": "2.0.1", 912 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 913 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" 914 | }, 915 | "react": { 916 | "version": "18.2.0", 917 | "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", 918 | "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", 919 | "requires": { 920 | "loose-envify": "^1.1.0" 921 | } 922 | }, 923 | "react-dom": { 924 | "version": "18.2.0", 925 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", 926 | "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", 927 | "requires": { 928 | "loose-envify": "^1.1.0", 929 | "scheduler": "^0.23.0" 930 | } 931 | }, 932 | "readable-stream": { 933 | "version": "2.3.7", 934 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", 935 | "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", 936 | "requires": { 937 | "core-util-is": "~1.0.0", 938 | "inherits": "~2.0.3", 939 | "isarray": "~1.0.0", 940 | "process-nextick-args": "~2.0.0", 941 | "safe-buffer": "~5.1.1", 942 | "string_decoder": "~1.1.1", 943 | "util-deprecate": "~1.0.1" 944 | } 945 | }, 946 | "regexparam": { 947 | "version": "1.3.0", 948 | "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-1.3.0.tgz", 949 | "integrity": "sha512-6IQpFBv6e5vz1QAqI+V4k8P2e/3gRrqfCJ9FI+O1FLQTO+Uz6RXZEZOPmTJ6hlGj7gkERzY5BRCv09whKP96/g==" 950 | }, 951 | "safe-buffer": { 952 | "version": "5.1.2", 953 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 954 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 955 | }, 956 | "scheduler": { 957 | "version": "0.23.0", 958 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", 959 | "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", 960 | "requires": { 961 | "loose-envify": "^1.1.0" 962 | } 963 | }, 964 | "source-map-js": { 965 | "version": "1.0.2", 966 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", 967 | "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" 968 | }, 969 | "streamsearch": { 970 | "version": "1.1.0", 971 | "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", 972 | "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" 973 | }, 974 | "string_decoder": { 975 | "version": "1.1.1", 976 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 977 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 978 | "requires": { 979 | "safe-buffer": "~5.1.0" 980 | } 981 | }, 982 | "styled-jsx": { 983 | "version": "5.1.0", 984 | "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.0.tgz", 985 | "integrity": "sha512-/iHaRJt9U7T+5tp6TRelLnqBqiaIT0HsO0+vgyj8hK2KUk7aejFqRrumqPUlAqDwAj8IbS/1hk3IhBAAK/FCUQ==", 986 | "requires": { 987 | "client-only": "0.0.1" 988 | } 989 | }, 990 | "trouter": { 991 | "version": "3.2.0", 992 | "resolved": "https://registry.npmjs.org/trouter/-/trouter-3.2.0.tgz", 993 | "integrity": "sha512-rLLXbhTObLy2MBVjLC+jTnoIKw99n0GuJs9ov10J870vDw5qhTurPzsDrudNtBf5w/CZ9ctZy2p2IMmhGcel2w==", 994 | "requires": { 995 | "regexparam": "^1.3.0" 996 | } 997 | }, 998 | "tslib": { 999 | "version": "2.4.1", 1000 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", 1001 | "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" 1002 | }, 1003 | "type-is": { 1004 | "version": "1.6.18", 1005 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1006 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1007 | "requires": { 1008 | "media-typer": "0.3.0", 1009 | "mime-types": "~2.1.24" 1010 | } 1011 | }, 1012 | "typedarray": { 1013 | "version": "0.0.6", 1014 | "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", 1015 | "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" 1016 | }, 1017 | "use-sync-external-store": { 1018 | "version": "1.2.0", 1019 | "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", 1020 | "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", 1021 | "requires": {} 1022 | }, 1023 | "util-deprecate": { 1024 | "version": "1.0.2", 1025 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1026 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" 1027 | }, 1028 | "xtend": { 1029 | "version": "4.0.2", 1030 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 1031 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" 1032 | } 1033 | } 1034 | } 1035 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openai-whisper", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "pages/index.js", 6 | "scripts": { 7 | "dev": "next dev -p 3006", 8 | "build": "next build", 9 | "start": "next start", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "multer": "^1.4.5-lts.1", 17 | "next": "^13.0.1", 18 | "next-connect": "^0.13.0", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import App from 'next/app'; 2 | import Head from 'next/head'; 3 | import '../styles/app.css'; 4 | 5 | export default function MyApp({ Component, pageProps }) { 6 | 7 | const siteTitle = process.env.siteTitle 8 | const props = { 9 | ...pageProps 10 | } 11 | 12 | return ( 13 | <> 14 | 15 | { siteTitle } 16 | 17 | 18 | 19 | 20 | ) 21 | } 22 | 23 | MyApp.getInitialProps = async (appContext) => { 24 | const appProps = await App.getInitialProps(appContext); 25 | return { 26 | ...appProps 27 | } 28 | } -------------------------------------------------------------------------------- /pages/api/transcribe.js: -------------------------------------------------------------------------------- 1 | import nextConnect from 'next-connect' 2 | import multer from 'multer' 3 | import { exec } from 'child_process' 4 | import getConfig from 'next/config' 5 | 6 | const upload = multer({ 7 | storage: multer.diskStorage({ 8 | destination: 'public/uploads', 9 | filename: (req, file, cb) => cb(null, `tmp-${file.originalname}`), 10 | }), 11 | }) 12 | 13 | const apiRoute = nextConnect({ 14 | onError(error, req, res) { 15 | res.status(501).json({error: `Some error '${error}' happen`}) 16 | }, 17 | onNoMatch(req, res) { 18 | res.status(405).json({error: `Method '${req.method}' not allowed`}) 19 | }, 20 | }) 21 | 22 | const uploadMiddleware = upload.single('file') 23 | 24 | apiRoute.use(uploadMiddleware) 25 | 26 | const { serverRuntimeConfig } = getConfig() 27 | 28 | apiRoute.post((req, res) => { 29 | 30 | const options = JSON.parse(req.body.options) 31 | 32 | const filename = req.file.path 33 | const outputDir = serverRuntimeConfig.PROJECT_ROOT + '/public/uploads' 34 | 35 | let sCommand = `whisper './${filename}' --model ${options.model} --language ${options.language} --task ${options.task} --output_dir '${outputDir}'` 36 | 37 | exec(sCommand, (err, stdout, stderr) => { 38 | if (err) { 39 | res.send({ status: 300, error: err, out: null, file: null }) 40 | } else { 41 | res.send({ status: 200, error: stderr, out: stdout, file: req.file }) 42 | } 43 | }) 44 | 45 | }) 46 | 47 | export default apiRoute 48 | 49 | export const config = { 50 | api: { 51 | bodyParser: false, 52 | } 53 | } -------------------------------------------------------------------------------- /pages/components/animatedBars.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classes from './animatedBars.module.css' 3 | 4 | export default function AnimatedBars(props) { 5 | 6 | const [bars, setBars] = React.useState(Array(8).fill(1)) 7 | 8 | React.useEffect(() => { 9 | 10 | let timer = null 11 | 12 | if(props.start) { 13 | timer = setInterval(() => { 14 | setBars((prev) => { 15 | let tmp = prev.map((v, i) => 1 + Math.round((1 + (16 * Math.sin((i/7)*Math.PI))) * Math.random())) 16 | return tmp 17 | }) 18 | }, 100) 19 | } else { 20 | setBars(Array(8).fill(1)) 21 | } 22 | 23 | return () => { 24 | clearInterval(timer) 25 | } 26 | 27 | }, [props.start]) 28 | 29 | return ( 30 |
31 | { 32 | bars.map((item, index) => { 33 | return ( 34 |
41 | ) 42 | }) 43 | } 44 |
45 | ) 46 | } -------------------------------------------------------------------------------- /pages/components/animatedBars.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | height: 18px; 4 | display: flex; 5 | justify-content: space-between; 6 | align-items: center; 7 | } 8 | .bar { 9 | position: relative; 10 | border-radius: 3px; 11 | background-color: #FFD167; 12 | background-image: linear-gradient(#FFD167, #FFA967); 13 | width: calc((100% - 9px)/8); 14 | } -------------------------------------------------------------------------------- /pages/components/arrow.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Arrow(props) { 4 | return ( 5 | 6 | 7 | 8 | ) 9 | } -------------------------------------------------------------------------------- /pages/components/dialog.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classes from './dialog.module.css' 3 | import IconButton from './iconbutton' 4 | 5 | function Dialog(props) { 6 | 7 | return ( 8 |
9 |
10 |

Settings

11 | 12 |
×
13 |
14 |
15 |
16 |
17 |
18 | 19 | 30 |
31 |
32 | 33 | 42 |
43 |
44 | 45 | 53 |
54 |
55 | 56 | 60 |
61 |
62 | 63 | 67 |
68 |
69 |
70 |
71 | ) 72 | } 73 | 74 | export default Dialog -------------------------------------------------------------------------------- /pages/components/dialog.module.css: -------------------------------------------------------------------------------- 1 | .dialog { 2 | box-shadow: rgba(99, 99, 99, 0.2) 0px 2px 8px 0px; 3 | background-color: #555; 4 | border-radius: 5px; 5 | position: relative; 6 | width: 80%; 7 | max-width: 700px; 8 | height: 220px; 9 | overflow: hidden; 10 | } 11 | .header { 12 | position: relative; 13 | height: 40px; 14 | padding: 0 0.3em 0 1em; 15 | box-sizing: border-box; 16 | display: flex; 17 | justify-content: space-between; 18 | align-items: center; 19 | } 20 | .title { 21 | margin: 0; 22 | } 23 | .times { 24 | background-color: #4c4c4c; 25 | border-radius: 50%; 26 | font-size: 1em; 27 | font-weight: 600; 28 | width: 24px; 29 | height: 24px; 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | } 34 | .main { 35 | position: relative; 36 | height: calc(100% - 40px); 37 | display: flex; 38 | justify-content: center; 39 | align-items: center; 40 | } 41 | .form { 42 | position: relative; 43 | width: calc(100% - 2em); 44 | } 45 | .item { 46 | position: relative; 47 | margin-bottom: 5px; 48 | display: flex; 49 | justify-content: space-between; 50 | } 51 | .item label { 52 | font-size: 1em; 53 | color: #fff; 54 | } 55 | .item select { 56 | width: 120px; 57 | font-size: 1em; 58 | } 59 | 60 | @media screen and (min-width: 800px) { 61 | .form { 62 | display: flex; 63 | justify-content: space-between; 64 | } 65 | .item { 66 | flex-direction: column; 67 | margin-bottom: 0; 68 | } 69 | } -------------------------------------------------------------------------------- /pages/components/iconbutton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classes from './iconbutton.module.css' 3 | 4 | function IconButton({ disabled, children, size = 24, onClick }) { 5 | 6 | const handleClick = () => { 7 | if(!disabled) { 8 | onClick() 9 | } 10 | } 11 | 12 | return ( 13 |
17 | { children } 18 |
19 | ) 20 | } 21 | 22 | export default IconButton -------------------------------------------------------------------------------- /pages/components/iconbutton.module.css: -------------------------------------------------------------------------------- 1 | .iconButton { 2 | position: relative; 3 | border-radius: 50%; 4 | width: 24px; 5 | height: 24px; 6 | cursor: pointer; 7 | } -------------------------------------------------------------------------------- /pages/components/message.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import classes from './message.module.css' 4 | 5 | import Arrow from './arrow' 6 | 7 | import IconButton from './iconbutton' 8 | 9 | import Progress from './progress' 10 | 11 | import { getDateTimeFromMS } from '../lib/utils' 12 | 13 | function removeHour(str) { 14 | let token = str.split(":") 15 | return token.length > 2 ? [token[1], token[2]].join(":") : [token[0], token[1]].join(":") 16 | } 17 | 18 | function formatText(text) { 19 | 20 | const token = text.split("] ") 21 | 22 | let tmp_time = token[0].replaceAll(',', '.') 23 | tmp_time = tmp_time.slice(1) 24 | 25 | let stoken = tmp_time.split(" --> ") 26 | let time1 = stoken[0].trim() 27 | let time2 = stoken[1].trim() 28 | 29 | time1 = removeHour(time1) 30 | time2 = removeHour(time2) 31 | 32 | const text_time = `[${time1} --> ${time2}]` 33 | const text_text = token.length > 1 ? token[1] : '' 34 | 35 | return { 36 | duration: text_time, 37 | text: text_text, 38 | } 39 | 40 | } 41 | 42 | function Message({ id, texts, duration, mode, disabled, onClick }) { 43 | 44 | const [count, setCount] = React.useState(0) 45 | const [value, setValue] = React.useState(0) 46 | 47 | React.useEffect(() => { 48 | 49 | let timer = null 50 | 51 | if(mode) { 52 | 53 | const interval = (duration * 1000)/100 54 | const delta = duration / 100 55 | 56 | timer = setInterval(() => { 57 | 58 | setCount(c => c + 1) 59 | setValue(v => v + delta) 60 | 61 | }, interval) 62 | 63 | } 64 | 65 | return () => { 66 | 67 | setCount(0) 68 | setValue(0) 69 | clearInterval(timer) 70 | } 71 | 72 | }, [mode, duration]) 73 | 74 | let now = id.replace('tmp-file', '').replace('.m4a', '') 75 | let display_date = getDateTimeFromMS(now) 76 | let display_value = Math.round(10 * value)/10 77 | 78 | return ( 79 |
80 |
{ display_date }
81 |
82 |
83 | { texts.map((rawtext, index) => { 84 | 85 | const { duration, text } = formatText(rawtext) 86 | 87 | return ( 88 |

89 | { duration } 90 | { text } 91 |

92 | ) 93 | }) } 94 |
95 |
96 |
97 | 107 |
108 |
109 | { 110 | mode === 0 && 111 | onClick(id)}> 112 | 113 | 114 | } 115 |
116 |
117 |
118 |
119 | ) 120 | } 121 | 122 | export default Message -------------------------------------------------------------------------------- /pages/components/message.module.css: -------------------------------------------------------------------------------- 1 | .message { 2 | box-shadow: rgba(99, 99, 99, 0.2) 0px 2px 8px 0px; 3 | background-color: #5559; 4 | border-radius: 0.2em; 5 | position: relative; 6 | padding: 1em; 7 | margin-bottom: 5px; 8 | } 9 | .message:last-child { 10 | margin-bottom: 0; 11 | } 12 | .datetime { 13 | font-size: 0.7em; 14 | color: #e6e6e6; 15 | } 16 | .inner { 17 | display: flex; 18 | justify-content: space-between; 19 | align-items: center; 20 | } 21 | .text { 22 | margin-right: 10px; 23 | } 24 | .text .item { 25 | margin: 0; 26 | } 27 | .textTime { 28 | margin-right: 0.5em; 29 | font-size: 0.8em; 30 | color: #AAA; 31 | } 32 | .textText { 33 | color: #FFF; 34 | } 35 | 36 | .action { 37 | position: relative; 38 | width: 32px; 39 | height: 32px; 40 | box-sizing: border-box; 41 | } 42 | 43 | .actionBottom { 44 | position: absolute; 45 | left: 0; 46 | top: 0; 47 | width: 32px; 48 | height: 32px; 49 | box-sizing: border-box; 50 | z-index: 1; 51 | } 52 | .actionTop { 53 | position: absolute; 54 | left: 0; 55 | top: 0; 56 | width: 32px; 57 | height: 32px; 58 | box-sizing: border-box; 59 | z-index: 2; 60 | display: flex; 61 | justify-content: center; 62 | align-items: center; 63 | } 64 | -------------------------------------------------------------------------------- /pages/components/microphone.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Microphone(props) { 4 | return ( 5 | 6 | 7 | 8 | ) 9 | } -------------------------------------------------------------------------------- /pages/components/microphoneOff.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function MicrophoneOff(props) { 4 | return ( 5 | 6 | 7 | 8 | ) 9 | } -------------------------------------------------------------------------------- /pages/components/pause.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Pause(props) { 4 | return ( 5 | 6 | 7 | 8 | ) 9 | } -------------------------------------------------------------------------------- /pages/components/play.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Play(props) { 4 | return ( 5 | 6 | 7 | 8 | ) 9 | } -------------------------------------------------------------------------------- /pages/components/progress.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Progress({ size, lineWidth, displayOff, displayOther = false, displayValue, value, lineColor, textColor, backgroundColor, progressColor }) { 4 | 5 | let p1 = value < 25 ? 90 - Math.round((value / 25)*90) : 0 6 | let p2 = value < 25 ? 90 : value >= 50 ? 0 : 90 - Math.round(((value - 25) / 25)*90) 7 | let p3 = value < 50 ? 90 : value >= 75 ? 0 : 90 - Math.round(((value - 50) / 25)*90) 8 | let p4 = value < 75 ? 90 : value >= 100 ? 0 : 90 - Math.round(((value - 75) / 25)*90) 9 | 10 | return ( 11 | <> 12 |
13 |
14 |
17 |
20 |
23 |
26 |
27 | { 28 | displayOther &&
{ displayValue }
29 | } 30 | { 31 | !displayOff &&
{ value }%
32 | } 33 |
34 |
35 |
36 | 91 | 92 | ) 93 | } 94 | 95 | Progress.defaultProps = { 96 | size: 100, 97 | lineWidth: 10, 98 | displayOff: false, 99 | textColor: '#FFF', 100 | backgroundColor: '#333', 101 | progressColor: '#FFD167', 102 | lineColor: '#555', 103 | value: 0, 104 | } -------------------------------------------------------------------------------- /pages/components/progress.module.css: -------------------------------------------------------------------------------- 1 | .progress { 2 | position: relative; 3 | width: 100px; 4 | height: 100px; 5 | box-sizing: border-box; 6 | overflow: hidden; 7 | } 8 | .inner { 9 | position: relative; 10 | width: 100%; 11 | height: 100%; 12 | border-radius: 50%; 13 | overflow: hidden; 14 | box-sizing: border-box; 15 | } 16 | .display { 17 | background-color: #333; 18 | border-radius: 50%; 19 | position: absolute; 20 | left: 10px; 21 | top: 10px; 22 | width: calc(100% - 20px); 23 | height: calc(100% - 20px); 24 | z-index: 2; 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | } 29 | .display span { 30 | font-family: helvetica, arial, sans-serif; 31 | font-size: 1.2em; 32 | font-weight: 600; 33 | } -------------------------------------------------------------------------------- /pages/components/settings.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Settings(props) { 4 | return ( 5 | 6 | 7 | 8 | ) 9 | } -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classes from './index.module.css' 3 | import Message from './components/message' 4 | import Progress from './components/progress' 5 | import IconButton from './components/iconbutton' 6 | import Microphone from './components/microphone' 7 | import MicrophoneOff from './components/microphoneOff' 8 | import Settings from './components/settings' 9 | 10 | import Dialog from './components/dialog' 11 | 12 | import AnimatedBars from './components/animatedBars' 13 | 14 | import { getFilesFromUpload } from './lib/upload' 15 | 16 | const sendData = async (file, options, signal) => { 17 | 18 | let formData = new FormData() 19 | formData.append("file", file) 20 | formData.append("options", JSON.stringify(options)) 21 | 22 | try { 23 | 24 | const resp = await fetch("/api/transcribe", { 25 | method: "POST", 26 | headers: { 27 | 'Accept': 'application/json', 28 | }, 29 | body: formData, 30 | signal: signal, 31 | }) 32 | 33 | return await resp.json() 34 | 35 | } catch(err) { 36 | console.log(err) 37 | } 38 | 39 | } 40 | 41 | const formatData = (data) => { 42 | 43 | return data.split("\n").filter(item => item.length > 0).filter(item => item.indexOf('[') === 0) 44 | 45 | } 46 | 47 | export async function getServerSideProps(context) { 48 | 49 | const files = getFilesFromUpload() 50 | 51 | return { 52 | props: { prev: files }, 53 | } 54 | } 55 | 56 | class Page extends React.Component { 57 | 58 | constructor(props) { 59 | 60 | super(props) 61 | 62 | this.audioRef = React.createRef() 63 | 64 | this.state = { 65 | 66 | data: this.props.prev || [], 67 | 68 | progress: 0, 69 | selected: '', 70 | error: false, 71 | started: false, 72 | sendStatus: 0, 73 | 74 | recording: false, 75 | countDown: false, 76 | count: 0, 77 | 78 | openDialog: false, 79 | duration: 5, 80 | model: "tiny", 81 | language: "Japanese", 82 | task: "translate", 83 | 84 | playDuration: 0, 85 | minDecibels: -45, 86 | maxPause: 2500, 87 | } 88 | 89 | this.mediaRec = null 90 | this.chunks = [] 91 | 92 | this.MAX_COUNT = 10 93 | this.MIN_DECIBELS = -45 94 | this.MAX_PAUSE = 3000 95 | 96 | this.animFrame = null 97 | this.countTimer = null 98 | this.audioDomRef = null 99 | this.abortController = null 100 | 101 | this.handlePlay = this.handlePlay.bind(this) 102 | this.handleStart = this.handleStart.bind(this) 103 | 104 | this.handleStream = this.handleStream.bind(this) 105 | this.handleError = this.handleError.bind(this) 106 | this.handleData = this.handleData.bind(this) 107 | this.handleStop = this.handleStop.bind(this) 108 | 109 | this.handleSettings = this.handleSettings.bind(this) 110 | this.handleCloseSettings = this.handleCloseSettings.bind(this) 111 | 112 | } 113 | 114 | componentWillUnmount() { 115 | 116 | try { 117 | 118 | window.cancelAnimationFrame(this.animFrame) 119 | 120 | if(this.abortController) { 121 | this.abortController.abort() 122 | } 123 | 124 | } catch(err) { 125 | 126 | console.log(err) 127 | 128 | } 129 | 130 | } 131 | 132 | componentDidMount() { 133 | 134 | try { 135 | 136 | let rawdata = localStorage.getItem('openai-whisper-settings') 137 | if(rawdata) { 138 | 139 | const options = JSON.parse(rawdata) 140 | 141 | this.setState({ 142 | duration: parseInt(options.duration), 143 | model: options.model, 144 | language: options.language, 145 | task: options.task, 146 | minDecibels: options.hasOwnProperty('minDecibels') ? parseInt(options.minDecibels) : this.MIN_DECIBELS, 147 | maxPause: options.hasOwnProperty('maxPause') ? parseInt(options.maxPause) : this.MAX_PAUSE, 148 | }) 149 | } 150 | 151 | } catch(err) { 152 | // 153 | } 154 | 155 | if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { 156 | 157 | const options = { audio: true } 158 | navigator.mediaDevices.getUserMedia(options).then(this.handleStream).catch(this.handleError) 159 | 160 | } else { 161 | 162 | console.log("Media devices not supported") 163 | 164 | this.setState({ 165 | error: true, 166 | }) 167 | 168 | } 169 | 170 | this.abortController = new AbortController() 171 | 172 | } 173 | 174 | handleUpdateOptions({ duration, model, language, task, minDecibels, maxPause }) { 175 | 176 | let options = { 177 | duration: this.state.duration, 178 | model: this.state.model, 179 | language: this.state.language, 180 | task: this.state.task, 181 | minDecibels: this.state.minDecibels, 182 | maxPause: this.state.maxPause, 183 | } 184 | 185 | if(maxPause) { 186 | this.setState({ 187 | maxPause: parseInt(maxPause), 188 | }) 189 | options.maxPause = parseInt(maxPause) 190 | } 191 | 192 | if(minDecibels) { 193 | this.setState({ 194 | minDecibels: parseInt(minDecibels), 195 | }) 196 | options.minDecibels = parseInt(minDecibels) 197 | } 198 | 199 | if(duration) { 200 | this.setState({ 201 | duration: duration, 202 | }) 203 | options.duration = duration 204 | } 205 | 206 | if(model) { 207 | this.setState({ 208 | model: model, 209 | }) 210 | options.model = model 211 | } 212 | 213 | if(language) { 214 | this.setState({ 215 | language: language, 216 | }) 217 | options.language = language 218 | } 219 | 220 | if(task) { 221 | this.setState({ 222 | task: task, 223 | }) 224 | options.task = task 225 | } 226 | 227 | localStorage.setItem('openai-whisper-settings', JSON.stringify(options)) 228 | 229 | } 230 | 231 | handleCloseSettings() { 232 | this.setState({ 233 | openDialog: false, 234 | }) 235 | } 236 | 237 | handleSettings() { 238 | if(this.state.started || this.state.sendStatus > 0) return 239 | this.setState({ 240 | openDialog: true, 241 | }) 242 | } 243 | 244 | handleError(error) { 245 | 246 | console.log(error) 247 | 248 | this.setState({ 249 | error: true, 250 | }) 251 | } 252 | 253 | handleStream(stream) { 254 | 255 | this.mediaRec = new MediaRecorder(stream) 256 | this.mediaRec.addEventListener('dataavailable', this.handleData) 257 | this.mediaRec.addEventListener("stop", this.handleStop) 258 | 259 | this.checkAudioLevel(stream) 260 | 261 | } 262 | 263 | checkAudioLevel(stream) { 264 | 265 | const audioContext = new AudioContext() 266 | const audioStreamSource = audioContext.createMediaStreamSource(stream) 267 | const analyser = audioContext.createAnalyser() 268 | // by default maxDecibels is -30bB and throws INDEX_SIZE_ERR when minDecibels is the same 269 | analyser.maxDecibels = -10 270 | analyser.minDecibels = this.state.minDecibels 271 | audioStreamSource.connect(analyser) 272 | 273 | const bufferLength = analyser.frequencyBinCount 274 | const domainData = new Uint8Array(bufferLength) 275 | 276 | const detectSound = () => { 277 | 278 | let soundDetected = false 279 | 280 | analyser.getByteFrequencyData(domainData) 281 | 282 | for (let i = 0; i < bufferLength; i++) { 283 | if (domainData[i] > 0) { 284 | soundDetected = true 285 | } 286 | } 287 | 288 | if(soundDetected === true) { 289 | 290 | if(this.state.recording) { 291 | 292 | if(this.state.countDown) { 293 | 294 | clearInterval(this.countTimer) 295 | 296 | this.setState({ 297 | countDown: false, 298 | count: 0, 299 | }) 300 | 301 | } 302 | 303 | } else { 304 | 305 | if(this.state.started) { 306 | 307 | this.setState({ 308 | countDown: false, 309 | recording: true, 310 | count: 0, 311 | }) 312 | 313 | this.mediaRec.start() 314 | 315 | } 316 | 317 | } 318 | 319 | } else { 320 | 321 | if(this.state.recording) { 322 | 323 | if(this.state.countDown) { 324 | 325 | if(this.state.count >= this.state.maxPause) { 326 | 327 | if(this.state.started) { 328 | 329 | clearInterval(this.countTimer) 330 | 331 | this.setState({ 332 | countDown: false, 333 | count: 0, 334 | recording: false, 335 | }) 336 | 337 | this.mediaRec.stop() 338 | 339 | } 340 | 341 | } 342 | 343 | } else { 344 | 345 | this.setState({ 346 | count: 0, 347 | countDown: true, 348 | }) 349 | 350 | this.startCountDown() 351 | 352 | } 353 | 354 | } 355 | 356 | } 357 | 358 | this.animFrame = window.requestAnimationFrame(detectSound) 359 | 360 | } 361 | 362 | this.animFrame = window.requestAnimationFrame(detectSound) 363 | 364 | } 365 | 366 | startCountDown() { 367 | 368 | this.countTimer = setInterval(() => { 369 | 370 | this.setState((prev) => { 371 | return { 372 | ...prev, 373 | count: prev.count + 100, 374 | } 375 | }) 376 | 377 | }, 100) 378 | 379 | } 380 | 381 | 382 | handleData(e) { 383 | 384 | this.chunks.push(e.data) 385 | 386 | } 387 | 388 | sendAudioData(file) { 389 | 390 | this.setState((prev) => { 391 | return { 392 | ...prev, 393 | sendStatus: prev.sendStatus + 1, 394 | } 395 | }) 396 | 397 | sendData(file, { model: this.state.model, language: this.state.language, task: this.state.task }, this.abortController.signal).then(resp => { 398 | 399 | const _status = resp.status 400 | const _file = resp.file?.filename 401 | const _url = resp.file?.path 402 | const _out = resp.out 403 | 404 | if(_status === 200) { 405 | 406 | const items = formatData(_out) 407 | 408 | if(items.length > 0) { 409 | 410 | let d = this.state.data.slice(0) 411 | 412 | d.push({ id: _file, url: _url.replace('public/', '/'), texts: items }) 413 | 414 | this.setState((prev) => { 415 | let c = prev.sendStatus - 1 416 | return { 417 | ...prev, 418 | data: d, 419 | sendStatus: c < 0 ? 0 : c, 420 | } 421 | }) 422 | 423 | return 424 | 425 | } 426 | 427 | } 428 | 429 | this.setState((prev) => { 430 | let c = prev.sendStatus - 1 431 | return { 432 | ...prev, 433 | sendStatus: c < 0 ? 0 : c, 434 | } 435 | }) 436 | 437 | }).catch(error => { 438 | 439 | console.log(error) 440 | 441 | }) 442 | 443 | } 444 | 445 | handleStop() { 446 | 447 | const blob = new Blob(this.chunks, {type: 'audio/webm;codecs=opus'}) 448 | this.chunks = [] 449 | 450 | var file = new File([blob], `file${Date.now()}.m4a`); 451 | 452 | this.sendAudioData(file) 453 | 454 | } 455 | 456 | async getDuration(id) { 457 | 458 | this.audioDomRef.currentTime = 0 459 | this.audioDomRef.removeEventListener('timeupdate', this.getDuration) 460 | 461 | if(this.audioDomRef.duration === Infinity) { 462 | console.log("[Error] Cannot play audio data") 463 | return 464 | } 465 | 466 | this.setState({ 467 | playDuration: this.audioDomRef.duration, 468 | selected: id, 469 | }) 470 | 471 | try { 472 | await this.audioDomRef.play() 473 | } catch(err) { 474 | console.log(err) 475 | } 476 | 477 | setTimeout(() => { 478 | 479 | this.audioDomRef.remove() 480 | this.audioDomRef = null 481 | 482 | this.setState({ 483 | selected: '', 484 | }) 485 | 486 | }, Math.round(this.audioDomRef.duration * 1000)) 487 | } 488 | 489 | async handlePlay(id) { 490 | 491 | if(this.state.selected) return; 492 | 493 | const selitem = this.state.data.find(item => item.id === id) 494 | 495 | this.audioDomRef = new Audio() 496 | this.audioDomRef.type = "audio/mp4" 497 | 498 | this.audioDomRef.addEventListener('loadedmetadata', async () => { 499 | 500 | if(this.audioDomRef.duration === Infinity) { 501 | 502 | this.audioDomRef.currentTime = 1e101 503 | this.audioDomRef.addEventListener('timeupdate', this.getDuration(id)) 504 | 505 | } else { 506 | 507 | this.setState({ 508 | playDuration: this.audioDomRef.duration, 509 | selected: id, 510 | }) 511 | 512 | try { 513 | await this.audioDomRef.play() 514 | } catch(err) { 515 | console.log(err) 516 | } 517 | 518 | setTimeout(() => { 519 | 520 | this.audioDomRef.remove() 521 | this.audioDomRef = null 522 | 523 | this.setState({ 524 | selected: '', 525 | }) 526 | 527 | }, Math.round(this.audioDomRef.duration * 1000)) 528 | 529 | } 530 | 531 | }) 532 | 533 | this.audioDomRef.src = selitem.url 534 | 535 | } 536 | 537 | handleStart() { 538 | 539 | if(this.state.error) return 540 | 541 | if(this.state.started) { 542 | 543 | clearInterval(this.countTimer) 544 | 545 | try { 546 | if(this.state.recording) { 547 | this.mediaRec.stop() 548 | } 549 | } catch(err) { 550 | console.log(err) 551 | } 552 | 553 | this.setState({ 554 | recording: false, 555 | countDown: false, 556 | count: 0, 557 | progress: 0, 558 | started: false, 559 | }) 560 | 561 | } else { 562 | 563 | this.setState({ 564 | progress: 0, 565 | started: true, 566 | }) 567 | 568 | } 569 | 570 | } 571 | 572 | render() { 573 | 574 | const display_data = this.state.data.sort((a, b) => { 575 | if(a.id > b.id) return -1 576 | if(a.id < b.id) return 1 577 | return 0 578 | }) 579 | 580 | return ( 581 |
582 |
583 |
584 | { 585 | display_data.map((item) => { 586 | return ( 587 | 0 && this.state.selected === item.id ? 1 : 0} 593 | onClick={this.handlePlay} /> 594 | ) 595 | }) 596 | } 597 |
598 |
599 |
600 |
601 | 602 | 0 ? '#444' : '#656565'} /> 603 | 604 |
605 |
606 |
0 ? classes.indicator : classes.indicatorOff}>
607 |
608 |
609 |
610 |
611 | 612 |
613 |
616 | 617 | { this.state.error ? : } 618 | 619 |
620 | { 621 | this.state.started && 622 |
623 | 626 |
627 | } 628 |
629 |
630 |
631 |
{this.state.duration}s
632 |
633 |
634 | { 635 | this.state.openDialog && 636 |
637 | this.handleUpdateOptions({ maxPause })} 645 | onChangeMinDecibels={(minDecibels) => this.handleUpdateOptions({ minDecibels })} 646 | onChangeModel={(model) => this.handleUpdateOptions({ model })} 647 | onChangeLanguage={(language) => this.handleUpdateOptions({ language })} 648 | onChangeTask={(task) => this.handleUpdateOptions({ task })} 649 | /> 650 |
651 | } 652 | 653 |
654 | ) 655 | } 656 | } 657 | 658 | export default Page -------------------------------------------------------------------------------- /pages/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | height: 100vh; 4 | overflow: hidden; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | flex-direction: column; 9 | } 10 | 11 | .panelMessages { 12 | background-image: linear-gradient(#333, #2E2E2E); 13 | position: relative; 14 | width: 100vw; 15 | height: calc(100% - 200px); 16 | display: flex; 17 | justify-content: center; 18 | align-items: center; 19 | } 20 | .listMessages { 21 | position: relative; 22 | width: calc(100% - 10px); 23 | height: calc(100% - 10px); 24 | box-sizing: border-box; 25 | overflow-x: hidden; 26 | overflow-y: auto; 27 | } 28 | 29 | .indicator { 30 | background-color: #F66; 31 | width: 0.5em; 32 | height: 0.5em; 33 | border-radius: 50%; 34 | margin-left: 1em; 35 | animation: blink 0.5s ease infinite; 36 | } 37 | .indicatorOff { 38 | background-color: #222; 39 | width: 0.5em; 40 | height: 0.5em; 41 | border-radius: 50%; 42 | margin-left: 1em; 43 | } 44 | @keyframes blink { 45 | 0% { background-color: #F66; } 46 | 100% { background-color: #F06; } 47 | } 48 | 49 | .panelLeft, .panelRight { 50 | width: 3em; 51 | } 52 | .panelRight { 53 | text-align: right; 54 | } 55 | .period { 56 | margin-right: 1em; 57 | font-size: 0.8em; 58 | color: #777; 59 | visibility: hidden; 60 | } 61 | 62 | .panelControl { 63 | background-color: #333; 64 | position: relative; 65 | width: 100vw; 66 | height: 200px; 67 | display: flex; 68 | justify-content: space-between; 69 | align-items: center; 70 | padding-bottom: 3em; 71 | box-sizing: border-box; 72 | } 73 | .settings { 74 | position: absolute; 75 | right: 5px; 76 | top: 5px; 77 | } 78 | 79 | .centerContainer { 80 | position: relative; 81 | width: 100px; 82 | height: 100px; 83 | } 84 | .progress { 85 | position: absolute; 86 | left: 0; 87 | top: 0; 88 | width: 100%; 89 | height: 100%; 90 | z-index: 1; 91 | overflow: hidden; 92 | display: none; 93 | } 94 | .buttonCenter { 95 | border-width: 3px; 96 | border-style: solid; 97 | border-radius: 50%; 98 | position: absolute; 99 | left: 0; 100 | top: 0; 101 | width: 100%; 102 | height: 100%; 103 | z-index: 2; 104 | display: flex; 105 | justify-content: center; 106 | align-items: center; 107 | box-sizing: border-box; 108 | } 109 | .soundLevel { 110 | position: absolute; 111 | width: 46px; 112 | height: 18px; 113 | left: 27px; 114 | top: 69px; 115 | overflow: hidden; 116 | box-sizing: border-box; 117 | } 118 | 119 | .dialog { 120 | background-color: #2229; 121 | position: absolute; 122 | left: 0; 123 | top: 0; 124 | width: 100%; 125 | height: 100%; 126 | z-index: 5; 127 | display: flex; 128 | justify-content: center; 129 | align-items: center; 130 | } 131 | 132 | @media screen and (min-width: 600px) { 133 | 134 | .container { 135 | flex-direction: row; 136 | } 137 | .panelMessages { 138 | background-image: linear-gradient(to right, #333, #2E2E2E); 139 | width: calc(100vw - 200px); 140 | height: 100vh; 141 | } 142 | .listMessages { 143 | padding-bottom: 2em; 144 | } 145 | .panelControl { 146 | width: 200px; 147 | height: 100vh; 148 | } 149 | 150 | } -------------------------------------------------------------------------------- /pages/lib/upload.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import getConfig from 'next/config' 4 | 5 | export function getFilesFromUpload() { 6 | 7 | const { serverRuntimeConfig } = getConfig() 8 | 9 | const uploadDir = 'uploads' 10 | 11 | const dir = path.join(serverRuntimeConfig.PROJECT_ROOT, './public', uploadDir) 12 | 13 | let files = fs.readdirSync(dir).filter(item => item.indexOf(".DS_Store") < 0) 14 | 15 | let srtfiles = files.filter(item => fs.existsSync(`${dir}/${item}.srt`)) 16 | 17 | let prevData = srtfiles.map(item => { 18 | 19 | let id = item 20 | let url = `/uploads/${item}` 21 | 22 | let txt = fs.readFileSync(`${dir}/${item}.srt`, {encoding: 'utf8', flag: 'r'}) 23 | 24 | let tokens = txt.split("\n") 25 | let texts = [] 26 | 27 | let f = false 28 | let str = '' 29 | for (let i = 0; i < tokens.length; i++) { 30 | 31 | if(f) { 32 | 33 | str += ' ' + tokens[i] 34 | texts.push(str) 35 | 36 | f = false 37 | str = '' 38 | 39 | } else { 40 | 41 | // this is a crude way to detect the time range part 42 | if(tokens[i].indexOf('00:') >= 0) { 43 | str = `[${tokens[i].trim()}]` 44 | f = true 45 | } 46 | 47 | } 48 | 49 | } 50 | 51 | return { 52 | id, 53 | url, 54 | texts: texts, 55 | } 56 | 57 | }) 58 | 59 | return prevData.filter(item => item.texts.length > 0) 60 | } -------------------------------------------------------------------------------- /pages/lib/useStorage.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | // Reference: https://stackoverflow.com/questions/58844583/how-to-implement-fifo-queue-with-reactjsredux 4 | function immutablePush(arr, newEntry) { 5 | return [ ...arr, newEntry ] 6 | } 7 | 8 | function immutableShift(arr) { 9 | return arr.slice(1) 10 | } 11 | 12 | export function useStorage() { 13 | 14 | const storage = React.useRef([]) 15 | 16 | const storagePush = (item) => { 17 | storage.current = immutablePush(storage.current, item) 18 | } 19 | 20 | const storagePop = () => { 21 | const item = storage.current[0] 22 | storage.current = immutableShift(storage.current) 23 | return item 24 | } 25 | 26 | return [ storagePush, storagePop ] 27 | 28 | } -------------------------------------------------------------------------------- /pages/lib/utils.js: -------------------------------------------------------------------------------- 1 | export function formatNumber(n) { 2 | return n < 10 ? `0${n}` : n 3 | } 4 | 5 | export function getDateTimeFromMS(ms) { 6 | const date = new Date(parseInt(ms)) 7 | 8 | const year = date.getFullYear() 9 | let month = date.getMonth() + 1 10 | let day = date.getDate() 11 | 12 | month = formatNumber(month) 13 | day = formatNumber(day) 14 | 15 | let hour = date.getHours() 16 | let minute = date.getMinutes() 17 | let second = date.getSeconds() 18 | 19 | hour = formatNumber(hour) 20 | minute = formatNumber(minute) 21 | second = formatNumber(second) 22 | 23 | return [[year, month, day].join('/'), [hour, minute, second].join(':')].join(' ') 24 | } 25 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-whisper/e62be330d6c9aab57f93fe3e93f22c736fddbaba/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-whisper/e62be330d6c9aab57f93fe3e93f22c736fddbaba/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-whisper/e62be330d6c9aab57f93fe3e93f22c736fddbaba/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Whisper", 3 | "name": "OpenAI Whisper: Speech Transcriber & Translator", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-whisper/e62be330d6c9aab57f93fe3e93f22c736fddbaba/public/screenshot.png -------------------------------------------------------------------------------- /public/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-whisper/e62be330d6c9aab57f93fe3e93f22c736fddbaba/public/screenshot2.png -------------------------------------------------------------------------------- /public/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supershaneski/openai-whisper/e62be330d6c9aab57f93fe3e93f22c736fddbaba/public/screenshot3.png -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const { createServer } = require('https') 2 | const { parse } = require('url') 3 | const next = require('next') 4 | const fs = require('fs') 5 | const port = 3006 6 | 7 | const dev = process.env.NODE_ENV !== 'production' 8 | const app = next({ dev }) 9 | const handle = app.getRequestHandler() 10 | 11 | const httpsOptions = { 12 | key: fs.readFileSync('./key.pem'), 13 | cert: fs.readFileSync('./cert.pem') 14 | } 15 | 16 | app.prepare().then(() => { 17 | createServer(httpsOptions, (req, res) => { 18 | const parsedUrl = parse(req.url, true) 19 | handle(req, res, parsedUrl) 20 | }).listen(port, (err) => { 21 | if(err) throw err 22 | console.log('\x1b[35m%s\x1b[0m', 'event', `- started ssl server on https://localhost:${port}`) 23 | }) 24 | }) 25 | 26 | -------------------------------------------------------------------------------- /styles/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #333; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | body { 19 | margin: 0; 20 | } 21 | 22 | @media (prefers-color-scheme: light) { 23 | body { 24 | background-color: #F5F5F5; 25 | color: #333; 26 | } 27 | } --------------------------------------------------------------------------------