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