├── src ├── index.ts └── Player.ts ├── .gitignore ├── tsconfig.cjs.json ├── example ├── index.html └── index.js ├── tsconfig.json ├── package.json ├── LICENSE ├── README.md └── pnpm-lock.yaml /src/index.ts: -------------------------------------------------------------------------------- 1 | export { SpeechPlayer } from './Player'; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | example/openai.js -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "./lib/cjs", 6 | "declaration": true 7 | } 8 | } -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "module": "es2020", 5 | "declaration": true, 6 | // "removeComments": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "allowSyntheticDefaultImports": true, 10 | "target": "ES2018", 11 | "sourceMap": true, 12 | "outDir": "./lib/esm", 13 | "baseUrl": "./", 14 | "incremental": true, 15 | "skipLibCheck": true, 16 | "strictNullChecks": false, 17 | "noImplicitAny": false, 18 | "strictBindCallApply": false, 19 | "forceConsistentCasingInFileNames": false, 20 | "noFallthroughCasesInSwitch": false, 21 | "esModuleInterop": true, 22 | // "resolveJsonModule": true, 23 | "rootDir": "./src" 24 | }, 25 | "exclude": [ 26 | "example", 27 | "lib", 28 | "node_modules" 29 | ] 30 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openai-speech-stream-player", 3 | "version": "1.0.9", 4 | "description": "It's a player for play SSE streaming chunk from OpenAI audio speech API.", 5 | "main": "lib/cjs/index.js", 6 | "module": "lib/esm/index.js", 7 | "types": "lib/esm/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "import": "./lib/esm/index.js", 11 | "require": "./lib/cjs/index.js" 12 | } 13 | }, 14 | "homepage": "https://github.com/kvsur/openai-speech-stream-player", 15 | "scripts": { 16 | "build": "rm -rf lib && tsc -p tsconfig.json && tsc -p tsconfig.cjs.json", 17 | "start": "http-server ./" 18 | }, 19 | "keywords": [ 20 | "OpenAI Audio", 21 | "Speech API", 22 | "stream", 23 | "player", 24 | "SSE", 25 | "stream chunk player" 26 | ], 27 | "author": "", 28 | "license": "ISC", 29 | "devDependencies": { 30 | "typescript": "^5.9.3", 31 | "http-server": "14.1.1" 32 | }, 33 | "files": ["lib/", "package.json", "example/", "README.md", "LICENSE", "tsconfig.json", "tsconfig.cjs.json"] 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 kvsur 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import { SpeechPlayer } from '../lib/index.js'; 2 | /** 3 | * You need create a file named openai.js and export key. 4 | * Example: ```export const key = "your key";``` 5 | * 6 | * */ 7 | import { key } from './openai.js'; 8 | 9 | async function main() { 10 | const audioEl = document.querySelector('audio'); 11 | const player = new SpeechPlayer({ 12 | audio: audioEl, 13 | onPlaying: () => { }, 14 | onPause: () => { }, 15 | onChunkEnd: () => { }, 16 | mimeType: 'audio/mpeg', 17 | }); 18 | await player.init(); 19 | 20 | var myHeaders = new Headers(); 21 | myHeaders.append("Cache-Control", "no-store"); 22 | myHeaders.append("Content-Type", "application/json"); 23 | myHeaders.append("Authorization", `Bearer ${key}`); 24 | 25 | var raw = JSON.stringify({ 26 | "model": "tts-1", 27 | "input": "Do not share your API key with others or expose it in the browser or other client-side code. To protect your account's security, OpenAI may automatically disable any API key that has leaked publicly.", 28 | "voice": "shimmer", 29 | "response_format": "mp3", 30 | "speed": 1 31 | }); 32 | 33 | var requestOptions = { 34 | method: 'POST', 35 | headers: myHeaders, 36 | body: raw, 37 | redirect: 'follow' 38 | }; 39 | 40 | const response = await fetch("https://api.openai.com/v1/audio/speech", requestOptions); 41 | player.feedWithResponse(response); 42 | } 43 | 44 | const btn = document.querySelector('button'); 45 | 46 | btn.onclick = () => { 47 | main(); 48 | }; 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # openai-speech-stream-player 2 | It's a player for play SSE streaming chunk from OpenAI audio speech API. 3 | 4 | 5 | ### Usage 6 | 7 | ```typescript 8 | import { SpeechPlayer } from 'openai-speech-stream-player'; 9 | 10 | async function main() { 11 | const audioEl = document.querySelector('audio'); 12 | const player = new SpeechPlayer({ 13 | audio: audioEl, 14 | onPlaying: () => {}, 15 | onPause: () => {}, 16 | onChunkEnd: () => {}, 17 | mimeType: 'audio/mpeg', 18 | }); 19 | await player.init(); 20 | 21 | var myHeaders = new Headers(); 22 | myHeaders.append("Cache-Control", "no-store"); 23 | myHeaders.append("Content-Type", "application/json"); 24 | myHeaders.append("Authorization", "Bearer yourKeyHere"); 25 | 26 | var raw = JSON.stringify({ 27 | "model": "tts-1", 28 | "input": "Hi, What's your name?", 29 | "voice": "shimmer", 30 | "response_format": "mp3", 31 | "speed": 1 32 | }); 33 | 34 | var requestOptions = { 35 | method: 'POST', 36 | headers: myHeaders, 37 | body: raw, 38 | redirect: 'follow' 39 | }; 40 | 41 | const response = await fetch("https://api.openai.com/v1/audio/speech", requestOptions); 42 | player.feedWithResponse(response); 43 | } 44 | 45 | main(); 46 | 47 | ``` 48 | 49 | Or you can DIY with response DIY 50 | 51 | ```typescript 52 | async function* streamAsyncIterable(stream) { 53 | const reader = stream.getReader(); 54 | 55 | try { 56 | while (true) { 57 | const { done, value } = await reader.read(); 58 | if (done) { 59 | return; 60 | } 61 | yield value; 62 | } 63 | } finally { 64 | reader.releaseLock(); 65 | } 66 | } 67 | 68 | async function main() { 69 | // ... 70 | const response = await fetch("https://api.openai.com/v1/audio/speech", requestOptions); 71 | 72 | for await (const chunk of streamAsyncIterable(response.body)) { 73 | player.feed(chunk); 74 | } 75 | } 76 | 77 | ``` 78 | 79 | ### Others 80 | 81 | 1. how to deal stream chunk in NodeJs form OpenAI text-to-speech API? 82 | 83 | here is my solution(code with nestJs): 84 | 85 | ```typescript 86 | import { Injectable, Logger } from '@nestjs/common'; 87 | import type { SpeechParams } from 'src/service/audio/Audio'; 88 | import type { Response } from 'express'; 89 | import { GPTAudio } from '../service/audio/Audio'; 90 | 91 | @Injectable() 92 | export class AudioService { 93 | private readonly logger = new Logger(AudioService.name); 94 | private readonly audio = new GPTAudio(); 95 | 96 | async speech(parmas: SpeechParams, res: Response): Promise { 97 | this.logger.log(`[AudioService speech]\n${JSON.stringify(parmas)}`); 98 | res.setHeader('Content-Type', 'audio/mpeg'); 99 | const response = await this.audio.speech(parmas); 100 | const body: NodeJS.ReadableStream = response.body as any; 101 | 102 | if (!body.on || !body.read) { 103 | throw new Error('unsupported "fetch" implementation'); 104 | } 105 | 106 | body.on('readable', () => { 107 | let chunk: string | Buffer; 108 | while (null !== (chunk = body.read())) { 109 | res.write(chunk); 110 | } 111 | }); 112 | body.on('close', () => { 113 | res.end(); 114 | }); 115 | } 116 | } 117 | ``` -------------------------------------------------------------------------------- /src/Player.ts: -------------------------------------------------------------------------------- 1 | interface Options { 2 | onPlaying?: () => void; 3 | onPause?: () => void; 4 | onChunkEnd?: () => void; 5 | mimeType?: string; 6 | audio?: HTMLAudioElement; 7 | } 8 | 9 | /** 10 | * @typedef {{ 11 | * onPlaying?: () => void; 12 | * onPause?: () => void; 13 | * onChunkEnd?: () => void; 14 | * mimeType?: string; 15 | * audio?: HTMLAudioElement 16 | * }} Options 17 | */ 18 | class SpeechPlayer { 19 | /** @type { HTMLAudioElement } */ 20 | _audio: HTMLAudioElement; 21 | /** @type { MediaSource } */ 22 | mediaSource: MediaSource; 23 | /** @type { SourceBuffer } */ 24 | sourceBuffer: SourceBuffer; 25 | /** @type {() => void } */ 26 | initResolve: ((value: unknown) => void) | null; 27 | /** @type {Uint8Array[]} */ 28 | sourceBufferCache: Uint8Array[] = []; 29 | /** @type {boolean} */ 30 | destroyed: boolean = false; 31 | /** @type {boolean} */ 32 | mediaSourceOpened: boolean = false; 33 | /** @type {Options} */ 34 | options: Options = {}; 35 | /** @type { AudioContext } */ 36 | audioContext: AudioContext; 37 | /** @type { number } */ 38 | nextStartTime: number = 0; 39 | /** @type { Uint8Array } */ 40 | pendingBuffer: Uint8Array = new Uint8Array(0); 41 | /** @type { boolean } */ 42 | useAudioContext: boolean = false; 43 | 44 | get audio() { 45 | return this._audio; 46 | } 47 | 48 | set audio(audio) { 49 | this._audio = audio; 50 | } 51 | 52 | /** 53 | * @param { Options } options 54 | */ 55 | constructor(options?: Options) { 56 | if (options) { 57 | this.audio = options.audio || new Audio(); 58 | } 59 | this.options = options || {}; 60 | } 61 | 62 | static async *streamAsyncIterable(stream: ReadableStream) { 63 | const reader = stream.getReader(); 64 | 65 | try { 66 | while (true) { 67 | const { done, value } = await reader.read(); 68 | if (done) { 69 | return; 70 | } 71 | yield value; 72 | } 73 | } finally { 74 | reader.releaseLock(); 75 | } 76 | } 77 | 78 | async init() { 79 | this.destroyed = false; 80 | const mimeType = this.options?.mimeType ?? 'audio/mpeg'; 81 | // Check if MediaSource is supported and can handle the mimeType 82 | if (typeof MediaSource !== 'undefined' && MediaSource.isTypeSupported(mimeType)) { 83 | this.useAudioContext = false; 84 | return new Promise((resolve, reject) => { 85 | this.mediaSource = new MediaSource(); 86 | this.audio.src = URL.createObjectURL(this.mediaSource); 87 | this.initResolve = resolve; 88 | this.mediaSource.addEventListener('sourceopen', this.sourceOpenHandle.bind(this)); 89 | }); 90 | } else { 91 | // Fallback to AudioContext for iOS Safari or other incompatible browsers 92 | this.useAudioContext = true; 93 | const AudioContext = window.AudioContext || (window as any).webkitAudioContext; 94 | this.audioContext = new AudioContext(); 95 | this.nextStartTime = 0; 96 | this.pendingBuffer = new Uint8Array(0); 97 | return Promise.resolve(); 98 | } 99 | } 100 | 101 | sourceOpenHandle() { 102 | if (this.initResolve) { 103 | this.initResolve(''); 104 | this.initResolve = null; 105 | URL.revokeObjectURL(this.audio.src); 106 | 107 | this.sourceBuffer = this.mediaSource.addSourceBuffer(this.options?.mimeType ?? 'audio/mpeg'); 108 | let timer = 0; 109 | this.audio.addEventListener('playing', () => { 110 | this.options.onPlaying && this.options.onPlaying(); 111 | }); 112 | this.audio.addEventListener('pause', () => { 113 | this.options.onPause && this.options.onPause(); 114 | }); 115 | this.sourceBuffer.addEventListener('updateend', () => { 116 | timer && clearTimeout(timer); 117 | this.audio.paused && this.audio.play(); 118 | (!this.sourceBuffer.updating 119 | && this.sourceBufferCache.length) 120 | && this.sourceBuffer.appendBuffer(this.sourceBufferCache.shift()! as unknown as BufferSource); 121 | }); 122 | this.audio.addEventListener('waiting', () => { 123 | timer = setTimeout(() => { 124 | if (!this.sourceBuffer.updating 125 | && this.mediaSource.readyState === 'open' 126 | && this.sourceBufferCache.length === 0) { 127 | this.mediaSource.endOfStream(); 128 | this.options.onChunkEnd && this.options.onChunkEnd(); 129 | } 130 | }, 500); 131 | }); 132 | this.mediaSourceOpened = true; 133 | } 134 | } 135 | 136 | /** 137 | * Feed audio chunk data into player with SourceBuffer created from MediaSource 138 | * @param {Uint8Array} chunk 139 | */ 140 | feed(chunk: Uint8Array) { 141 | if (this.destroyed) throw new ReferenceError('SpeechPlayer has been destroyed.'); 142 | 143 | if (this.useAudioContext) { 144 | const newBuffer = new Uint8Array(this.pendingBuffer.length + chunk.length); 145 | newBuffer.set(this.pendingBuffer); 146 | newBuffer.set(chunk, this.pendingBuffer.length); 147 | this.pendingBuffer = newBuffer; 148 | this.processAudioContextQueue(); 149 | return; 150 | } 151 | 152 | if (!this.mediaSourceOpened) throw new Error('MediaSource not opened, please do this update init resolved.'); 153 | this.sourceBufferCache.push(chunk); 154 | !this.sourceBuffer.updating && this.sourceBuffer.appendBuffer(this.sourceBufferCache.shift()! as unknown as BufferSource); 155 | } 156 | 157 | async processAudioContextQueue() { 158 | if (this.pendingBuffer.length === 0) return; 159 | 160 | // Find the last potential sync word to ensure we don't cut in the middle of a frame 161 | // MP3 sync word is usually 0xFF followed by 0xE0 (11 bits set) 162 | let cutIndex = -1; 163 | for (let i = this.pendingBuffer.length - 2; i >= 0; i--) { 164 | if (this.pendingBuffer[i] === 0xFF && (this.pendingBuffer[i + 1] & 0xE0) === 0xE0) { 165 | cutIndex = i; 166 | break; 167 | } 168 | } 169 | 170 | if (cutIndex > 0) { 171 | const dataToDecode = this.pendingBuffer.slice(0, cutIndex); 172 | const remaining = this.pendingBuffer.slice(cutIndex); 173 | this.pendingBuffer = remaining; 174 | 175 | try { 176 | // Decode the audio data 177 | const audioBuffer = await this.audioContext.decodeAudioData(dataToDecode.buffer); 178 | this.schedulePlayback(audioBuffer); 179 | 180 | // Trigger onPlaying if it's the first chunk 181 | if (this.nextStartTime === 0 && this.options.onPlaying) { 182 | this.options.onPlaying(); 183 | } 184 | } catch (e) { 185 | console.warn("Audio decode failed, keeping data in buffer", e); 186 | // If decoding fails, we might have cut incorrectly or data is bad. 187 | // For now, we prepend the data back to pendingBuffer to try again with more data? 188 | // Or just drop it? Dropping is safer to avoid stuck loop. 189 | // But maybe we should just wait for more data. 190 | // Let's put it back. 191 | const recovered = new Uint8Array(dataToDecode.length + this.pendingBuffer.length); 192 | recovered.set(dataToDecode); 193 | recovered.set(this.pendingBuffer, dataToDecode.length); 194 | this.pendingBuffer = recovered; 195 | } 196 | } 197 | } 198 | 199 | schedulePlayback(buffer: AudioBuffer) { 200 | const source = this.audioContext.createBufferSource(); 201 | source.buffer = buffer; 202 | source.connect(this.audioContext.destination); 203 | 204 | let start = this.nextStartTime; 205 | if (start < this.audioContext.currentTime) { 206 | start = this.audioContext.currentTime; 207 | } 208 | source.start(start); 209 | this.nextStartTime = start + buffer.duration; 210 | 211 | source.onended = () => { 212 | // Check if we are done 213 | if (this.pendingBuffer.length === 0 && Math.abs(this.nextStartTime - this.audioContext.currentTime) < 0.1) { 214 | this.options.onChunkEnd && this.options.onChunkEnd(); 215 | } 216 | }; 217 | } 218 | 219 | /** 220 | * Feed audio chunk just with Fetch response and deal automaticlly. 221 | * @param {Response} response 222 | */ 223 | async feedWithResponse(response: Response) { 224 | for await (const chunk of SpeechPlayer.streamAsyncIterable(response.body as ReadableStream)) { 225 | this.feed(chunk); 226 | } 227 | } 228 | 229 | play(): Promise { 230 | return new Promise((resolve, reject) => { 231 | try { 232 | if (this.useAudioContext) { 233 | if (this.audioContext.state === 'suspended') { 234 | this.audioContext.resume().then(() => { 235 | this.options.onPlaying && this.options.onPlaying(); 236 | resolve(true); 237 | }); 238 | } else { 239 | resolve(true); 240 | } 241 | return; 242 | } 243 | 244 | if (this.paused) { 245 | this.audio.play(); 246 | const playHandle = () => { 247 | resolve(true); 248 | this.audio.removeEventListener('playing', playHandle); 249 | }; 250 | this.audio.addEventListener('playing', playHandle); 251 | } else { 252 | // audio not exist or audio status is playing will resolve false result. 253 | resolve(false); 254 | } 255 | } catch (error) { 256 | reject(error); 257 | } 258 | }); 259 | } 260 | 261 | pause(): Promise { 262 | return new Promise((resolve, reject) => { 263 | try { 264 | if (this.useAudioContext) { 265 | if (this.audioContext.state === 'running') { 266 | this.audioContext.suspend().then(() => { 267 | this.options.onPause && this.options.onPause(); 268 | resolve(true); 269 | }); 270 | } else { 271 | resolve(false); 272 | } 273 | return; 274 | } 275 | 276 | if (this.playing) { 277 | this.audio.pause(); 278 | const pauseHandle = () => { 279 | this.audio.removeEventListener('pause', pauseHandle); 280 | resolve(true); 281 | }; 282 | this.audio.addEventListener('pause', pauseHandle); 283 | // puase event must be fired before setTimeout. 284 | setTimeout(() => { 285 | resolve(this.paused); 286 | }, 0); 287 | } else { 288 | // audio not exist or audio status is paused will resolve false result. 289 | resolve(false); 290 | } 291 | } catch (error) { 292 | reject(error); 293 | } 294 | }); 295 | } 296 | 297 | get paused() { 298 | if (this.useAudioContext) { 299 | return this.audioContext.state === 'suspended' || this.audioContext.state === 'closed'; 300 | } 301 | return this.audio && this.audio.paused; 302 | } 303 | 304 | get playing() { 305 | return !this.paused; 306 | } 307 | 308 | /** 309 | * Destroy speechPlayer instance, if want play again, need do init method again. 310 | */ 311 | destroy() { 312 | if (this.useAudioContext) { 313 | this.audioContext.close(); 314 | this.destroyed = true; 315 | return; 316 | } 317 | if (this.audio && this.audio.paused === false) this.audio.pause(); 318 | this.destroyed = true; 319 | this.mediaSourceOpened = false; 320 | this.mediaSource && this.mediaSource.removeSourceBuffer(this.sourceBuffer as SourceBuffer); 321 | this.mediaSource && this.mediaSource.endOfStream(); 322 | this.sourceBuffer.abort(); 323 | this.sourceBufferCache.splice(0); 324 | } 325 | } 326 | 327 | export { SpeechPlayer }; 328 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | devDependencies: 11 | http-server: 12 | specifier: 14.1.1 13 | version: 14.1.1 14 | typescript: 15 | specifier: ^5.9.3 16 | version: 5.9.3 17 | 18 | packages: 19 | 20 | ansi-styles@4.3.0: 21 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 22 | engines: {node: '>=8'} 23 | 24 | async@3.2.6: 25 | resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} 26 | 27 | basic-auth@2.0.1: 28 | resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} 29 | engines: {node: '>= 0.8'} 30 | 31 | call-bind-apply-helpers@1.0.2: 32 | resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} 33 | engines: {node: '>= 0.4'} 34 | 35 | call-bound@1.0.4: 36 | resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} 37 | engines: {node: '>= 0.4'} 38 | 39 | chalk@4.1.2: 40 | resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 41 | engines: {node: '>=10'} 42 | 43 | color-convert@2.0.1: 44 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 45 | engines: {node: '>=7.0.0'} 46 | 47 | color-name@1.1.4: 48 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 49 | 50 | corser@2.0.1: 51 | resolution: {integrity: sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==} 52 | engines: {node: '>= 0.4.0'} 53 | 54 | debug@4.4.3: 55 | resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} 56 | engines: {node: '>=6.0'} 57 | peerDependencies: 58 | supports-color: '*' 59 | peerDependenciesMeta: 60 | supports-color: 61 | optional: true 62 | 63 | dunder-proto@1.0.1: 64 | resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} 65 | engines: {node: '>= 0.4'} 66 | 67 | es-define-property@1.0.1: 68 | resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} 69 | engines: {node: '>= 0.4'} 70 | 71 | es-errors@1.3.0: 72 | resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} 73 | engines: {node: '>= 0.4'} 74 | 75 | es-object-atoms@1.1.1: 76 | resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} 77 | engines: {node: '>= 0.4'} 78 | 79 | eventemitter3@4.0.7: 80 | resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} 81 | 82 | follow-redirects@1.15.11: 83 | resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} 84 | engines: {node: '>=4.0'} 85 | peerDependencies: 86 | debug: '*' 87 | peerDependenciesMeta: 88 | debug: 89 | optional: true 90 | 91 | function-bind@1.1.2: 92 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 93 | 94 | get-intrinsic@1.3.0: 95 | resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} 96 | engines: {node: '>= 0.4'} 97 | 98 | get-proto@1.0.1: 99 | resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} 100 | engines: {node: '>= 0.4'} 101 | 102 | gopd@1.2.0: 103 | resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} 104 | engines: {node: '>= 0.4'} 105 | 106 | has-flag@4.0.0: 107 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 108 | engines: {node: '>=8'} 109 | 110 | has-symbols@1.1.0: 111 | resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} 112 | engines: {node: '>= 0.4'} 113 | 114 | hasown@2.0.2: 115 | resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 116 | engines: {node: '>= 0.4'} 117 | 118 | he@1.2.0: 119 | resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} 120 | hasBin: true 121 | 122 | html-encoding-sniffer@3.0.0: 123 | resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} 124 | engines: {node: '>=12'} 125 | 126 | http-proxy@1.18.1: 127 | resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} 128 | engines: {node: '>=8.0.0'} 129 | 130 | http-server@14.1.1: 131 | resolution: {integrity: sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==} 132 | engines: {node: '>=12'} 133 | hasBin: true 134 | 135 | iconv-lite@0.6.3: 136 | resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} 137 | engines: {node: '>=0.10.0'} 138 | 139 | math-intrinsics@1.1.0: 140 | resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} 141 | engines: {node: '>= 0.4'} 142 | 143 | mime@1.6.0: 144 | resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} 145 | engines: {node: '>=4'} 146 | hasBin: true 147 | 148 | minimist@1.2.8: 149 | resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} 150 | 151 | ms@2.1.3: 152 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 153 | 154 | object-inspect@1.13.4: 155 | resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} 156 | engines: {node: '>= 0.4'} 157 | 158 | opener@1.5.2: 159 | resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} 160 | hasBin: true 161 | 162 | portfinder@1.0.38: 163 | resolution: {integrity: sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==} 164 | engines: {node: '>= 10.12'} 165 | 166 | qs@6.14.0: 167 | resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} 168 | engines: {node: '>=0.6'} 169 | 170 | requires-port@1.0.0: 171 | resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} 172 | 173 | safe-buffer@5.1.2: 174 | resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} 175 | 176 | safer-buffer@2.1.2: 177 | resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} 178 | 179 | secure-compare@3.0.1: 180 | resolution: {integrity: sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==} 181 | 182 | side-channel-list@1.0.0: 183 | resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} 184 | engines: {node: '>= 0.4'} 185 | 186 | side-channel-map@1.0.1: 187 | resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} 188 | engines: {node: '>= 0.4'} 189 | 190 | side-channel-weakmap@1.0.2: 191 | resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} 192 | engines: {node: '>= 0.4'} 193 | 194 | side-channel@1.1.0: 195 | resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} 196 | engines: {node: '>= 0.4'} 197 | 198 | supports-color@7.2.0: 199 | resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 200 | engines: {node: '>=8'} 201 | 202 | typescript@5.9.3: 203 | resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} 204 | engines: {node: '>=14.17'} 205 | hasBin: true 206 | 207 | union@0.5.0: 208 | resolution: {integrity: sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==} 209 | engines: {node: '>= 0.8.0'} 210 | 211 | url-join@4.0.1: 212 | resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} 213 | 214 | whatwg-encoding@2.0.0: 215 | resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} 216 | engines: {node: '>=12'} 217 | 218 | snapshots: 219 | 220 | ansi-styles@4.3.0: 221 | dependencies: 222 | color-convert: 2.0.1 223 | 224 | async@3.2.6: {} 225 | 226 | basic-auth@2.0.1: 227 | dependencies: 228 | safe-buffer: 5.1.2 229 | 230 | call-bind-apply-helpers@1.0.2: 231 | dependencies: 232 | es-errors: 1.3.0 233 | function-bind: 1.1.2 234 | 235 | call-bound@1.0.4: 236 | dependencies: 237 | call-bind-apply-helpers: 1.0.2 238 | get-intrinsic: 1.3.0 239 | 240 | chalk@4.1.2: 241 | dependencies: 242 | ansi-styles: 4.3.0 243 | supports-color: 7.2.0 244 | 245 | color-convert@2.0.1: 246 | dependencies: 247 | color-name: 1.1.4 248 | 249 | color-name@1.1.4: {} 250 | 251 | corser@2.0.1: {} 252 | 253 | debug@4.4.3: 254 | dependencies: 255 | ms: 2.1.3 256 | 257 | dunder-proto@1.0.1: 258 | dependencies: 259 | call-bind-apply-helpers: 1.0.2 260 | es-errors: 1.3.0 261 | gopd: 1.2.0 262 | 263 | es-define-property@1.0.1: {} 264 | 265 | es-errors@1.3.0: {} 266 | 267 | es-object-atoms@1.1.1: 268 | dependencies: 269 | es-errors: 1.3.0 270 | 271 | eventemitter3@4.0.7: {} 272 | 273 | follow-redirects@1.15.11: {} 274 | 275 | function-bind@1.1.2: {} 276 | 277 | get-intrinsic@1.3.0: 278 | dependencies: 279 | call-bind-apply-helpers: 1.0.2 280 | es-define-property: 1.0.1 281 | es-errors: 1.3.0 282 | es-object-atoms: 1.1.1 283 | function-bind: 1.1.2 284 | get-proto: 1.0.1 285 | gopd: 1.2.0 286 | has-symbols: 1.1.0 287 | hasown: 2.0.2 288 | math-intrinsics: 1.1.0 289 | 290 | get-proto@1.0.1: 291 | dependencies: 292 | dunder-proto: 1.0.1 293 | es-object-atoms: 1.1.1 294 | 295 | gopd@1.2.0: {} 296 | 297 | has-flag@4.0.0: {} 298 | 299 | has-symbols@1.1.0: {} 300 | 301 | hasown@2.0.2: 302 | dependencies: 303 | function-bind: 1.1.2 304 | 305 | he@1.2.0: {} 306 | 307 | html-encoding-sniffer@3.0.0: 308 | dependencies: 309 | whatwg-encoding: 2.0.0 310 | 311 | http-proxy@1.18.1: 312 | dependencies: 313 | eventemitter3: 4.0.7 314 | follow-redirects: 1.15.11 315 | requires-port: 1.0.0 316 | transitivePeerDependencies: 317 | - debug 318 | 319 | http-server@14.1.1: 320 | dependencies: 321 | basic-auth: 2.0.1 322 | chalk: 4.1.2 323 | corser: 2.0.1 324 | he: 1.2.0 325 | html-encoding-sniffer: 3.0.0 326 | http-proxy: 1.18.1 327 | mime: 1.6.0 328 | minimist: 1.2.8 329 | opener: 1.5.2 330 | portfinder: 1.0.38 331 | secure-compare: 3.0.1 332 | union: 0.5.0 333 | url-join: 4.0.1 334 | transitivePeerDependencies: 335 | - debug 336 | - supports-color 337 | 338 | iconv-lite@0.6.3: 339 | dependencies: 340 | safer-buffer: 2.1.2 341 | 342 | math-intrinsics@1.1.0: {} 343 | 344 | mime@1.6.0: {} 345 | 346 | minimist@1.2.8: {} 347 | 348 | ms@2.1.3: {} 349 | 350 | object-inspect@1.13.4: {} 351 | 352 | opener@1.5.2: {} 353 | 354 | portfinder@1.0.38: 355 | dependencies: 356 | async: 3.2.6 357 | debug: 4.4.3 358 | transitivePeerDependencies: 359 | - supports-color 360 | 361 | qs@6.14.0: 362 | dependencies: 363 | side-channel: 1.1.0 364 | 365 | requires-port@1.0.0: {} 366 | 367 | safe-buffer@5.1.2: {} 368 | 369 | safer-buffer@2.1.2: {} 370 | 371 | secure-compare@3.0.1: {} 372 | 373 | side-channel-list@1.0.0: 374 | dependencies: 375 | es-errors: 1.3.0 376 | object-inspect: 1.13.4 377 | 378 | side-channel-map@1.0.1: 379 | dependencies: 380 | call-bound: 1.0.4 381 | es-errors: 1.3.0 382 | get-intrinsic: 1.3.0 383 | object-inspect: 1.13.4 384 | 385 | side-channel-weakmap@1.0.2: 386 | dependencies: 387 | call-bound: 1.0.4 388 | es-errors: 1.3.0 389 | get-intrinsic: 1.3.0 390 | object-inspect: 1.13.4 391 | side-channel-map: 1.0.1 392 | 393 | side-channel@1.1.0: 394 | dependencies: 395 | es-errors: 1.3.0 396 | object-inspect: 1.13.4 397 | side-channel-list: 1.0.0 398 | side-channel-map: 1.0.1 399 | side-channel-weakmap: 1.0.2 400 | 401 | supports-color@7.2.0: 402 | dependencies: 403 | has-flag: 4.0.0 404 | 405 | typescript@5.9.3: {} 406 | 407 | union@0.5.0: 408 | dependencies: 409 | qs: 6.14.0 410 | 411 | url-join@4.0.1: {} 412 | 413 | whatwg-encoding@2.0.0: 414 | dependencies: 415 | iconv-lite: 0.6.3 416 | --------------------------------------------------------------------------------