echogarden 0.11.12 → 0.11.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/data/schemas/options.json +16 -0
  2. package/dist/api/Alignment.js +2 -2
  3. package/dist/api/Alignment.js.map +1 -1
  4. package/dist/api/Recognition.js +2 -2
  5. package/dist/api/Recognition.js.map +1 -1
  6. package/dist/api/Synthesis.js +5 -4
  7. package/dist/api/Synthesis.js.map +1 -1
  8. package/dist/api/Translation.js +2 -2
  9. package/dist/api/Translation.js.map +1 -1
  10. package/dist/audio/AudioUtilities.d.ts +1 -0
  11. package/dist/audio/AudioUtilities.js +25 -7
  12. package/dist/audio/AudioUtilities.js.map +1 -1
  13. package/dist/cli/CLI.js +2 -2
  14. package/dist/cli/CLI.js.map +1 -1
  15. package/dist/recognition/WhisperSTT.js +2 -2
  16. package/dist/recognition/WhisperSTT.js.map +1 -1
  17. package/dist/subtitles/Subtitles.d.ts +10 -7
  18. package/dist/subtitles/Subtitles.js +268 -207
  19. package/dist/subtitles/Subtitles.js.map +1 -1
  20. package/docs/Options.md +4 -2
  21. package/package.json +7 -6
  22. package/src/alignment/DTWMfccSequenceAlignment.ts +43 -0
  23. package/src/alignment/DTWSequenceAlignment.ts +121 -0
  24. package/src/alignment/DTWSequenceAlignmentWindowed.ts +210 -0
  25. package/src/alignment/LevenshteinSequenceAlignment.ts +126 -0
  26. package/src/alignment/SpeechAlignment.ts +488 -0
  27. package/src/api/API.ts +12 -0
  28. package/src/api/APIOptions.ts +15 -0
  29. package/src/api/Alignment.ts +329 -0
  30. package/src/api/Common.ts +16 -0
  31. package/src/api/Denoising.ts +120 -0
  32. package/src/api/LanguageDetection.ts +286 -0
  33. package/src/api/Recognition.ts +344 -0
  34. package/src/api/Synthesis.ts +1735 -0
  35. package/src/api/Translation.ts +143 -0
  36. package/src/api/Vad.ts +172 -0
  37. package/src/audio/AudioBufferConversion.ts +248 -0
  38. package/src/audio/AudioPlayer.ts +358 -0
  39. package/src/audio/AudioRecorder.ts +91 -0
  40. package/src/audio/AudioUtilities.ts +392 -0
  41. package/src/audio/SoxPath.ts +24 -0
  42. package/src/cli/CLI.ts +1360 -0
  43. package/src/cli/CLIConfigFile.ts +91 -0
  44. package/src/cli/CLILauncher.ts +26 -0
  45. package/src/cli/CLIOptionsSchema.ts +54 -0
  46. package/src/cli/CLIParser.ts +41 -0
  47. package/src/cli/CLIStarter.ts +40 -0
  48. package/src/codecs/FFMpegTranscoder.ts +214 -0
  49. package/src/codecs/TIMITCodec.ts +17 -0
  50. package/src/codecs/WaveCodec.ts +260 -0
  51. package/src/denoising/RNNoise.ts +95 -0
  52. package/src/dsp/BiquadFilter.ts +488 -0
  53. package/src/dsp/FFT.ts +187 -0
  54. package/src/dsp/MFCC.ts +227 -0
  55. package/src/dsp/MelSpectogram.ts +145 -0
  56. package/src/dsp/Rubberband.ts +249 -0
  57. package/src/dsp/Sonic.ts +59 -0
  58. package/src/dsp/SpeexResampler.ts +79 -0
  59. package/src/math/VectorMath.ts +812 -0
  60. package/src/nlp/ChineseSegmentation.ts +68 -0
  61. package/src/nlp/CompromiseNLP.ts +113 -0
  62. package/src/nlp/EspeakPhonemizer.ts +168 -0
  63. package/src/nlp/IPA.ts +139 -0
  64. package/src/nlp/JapaneseSegmentation.ts +53 -0
  65. package/src/nlp/Lexicon.ts +119 -0
  66. package/src/nlp/PhoneConversion.ts +508 -0
  67. package/src/nlp/Segmentation.ts +237 -0
  68. package/src/nlp/TextNormalizer.ts +160 -0
  69. package/src/recognition/AmazonTranscribeSTT.ts +112 -0
  70. package/src/recognition/AzureCognitiveServicesSTT.ts +76 -0
  71. package/src/recognition/GoogleCloudSTT.ts +92 -0
  72. package/src/recognition/SileroSTT.ts +173 -0
  73. package/src/recognition/VoskSTT.ts +112 -0
  74. package/src/recognition/WhisperSTT.ts +1518 -0
  75. package/src/server/Client.ts +297 -0
  76. package/src/server/Server.ts +178 -0
  77. package/src/server/ServerStarter.ts +12 -0
  78. package/src/server/Worker.ts +400 -0
  79. package/src/server/WorkerStarter.ts +38 -0
  80. package/src/speech-language-detection/SileroLanguageDetection.ts +105 -0
  81. package/src/subtitles/Subtitles.ts +478 -0
  82. package/src/synthesis/AwsPollyTTS.ts +78 -0
  83. package/src/synthesis/AzureCognitiveServicesTTS.ts +146 -0
  84. package/src/synthesis/CoquiServerTTS.ts +29 -0
  85. package/src/synthesis/ElevenLabsTTS.ts +104 -0
  86. package/src/synthesis/EspeakTTS.ts +552 -0
  87. package/src/synthesis/FliteTTS.ts +387 -0
  88. package/src/synthesis/GoogleCloudTTS.ts +112 -0
  89. package/src/synthesis/GoogleTranslateTTS.ts +210 -0
  90. package/src/synthesis/MicrosoftEdgeTTS.ts +298 -0
  91. package/src/synthesis/SamTTS.ts +30 -0
  92. package/src/synthesis/SapiTTS.ts +222 -0
  93. package/src/synthesis/StreamlabsPollyTTS.ts +114 -0
  94. package/src/synthesis/SvoxPicoTTS.ts +318 -0
  95. package/src/synthesis/VitsTTS.ts +734 -0
  96. package/src/tests/Test.ts +24 -0
  97. package/src/text-language-detection/FastTextLanguageDetection.ts +53 -0
  98. package/src/text-language-detection/TinyLDLanguageDetection.ts +16 -0
  99. package/src/typings/Fillers.d.ts +41 -0
  100. package/src/utilities/BinaryArrayConversion.ts +159 -0
  101. package/src/utilities/Compression.ts +91 -0
  102. package/src/utilities/FileDownloader.ts +201 -0
  103. package/src/utilities/FileSystem.ts +265 -0
  104. package/src/utilities/Hashing.ts +230 -0
  105. package/src/utilities/Locale.ts +119 -0
  106. package/src/utilities/Logger.ts +72 -0
  107. package/src/utilities/NdArrayUtilities.ts +31 -0
  108. package/src/utilities/ObjectUtilities.ts +169 -0
  109. package/src/utilities/OpenPromise.ts +13 -0
  110. package/src/utilities/PackageManager.ts +97 -0
  111. package/src/utilities/Queue.ts +17 -0
  112. package/src/utilities/RandomGenerator.ts +237 -0
  113. package/src/utilities/SignalChannel.ts +22 -0
  114. package/src/utilities/TarballMaker.ts +68 -0
  115. package/src/utilities/Timeline.ts +231 -0
  116. package/src/utilities/Timer.ts +93 -0
  117. package/src/utilities/Utilities.ts +574 -0
  118. package/src/utilities/WasmMemoryManager.ts +516 -0
  119. package/src/utilities/WebReader.ts +55 -0
  120. package/src/utilities/WikipediaReader.ts +41 -0
  121. package/src/voice-activity-detection/SileroVAD.ts +86 -0
  122. package/src/voice-activity-detection/WebRtcVAD.ts +76 -0
@@ -0,0 +1,24 @@
1
+ import { getRepetitionScoreRelativeToFirstSubstring, logToStderr, setupProgramTerminationListeners } from "../utilities/Utilities.js"
2
+ import { makeTarballsForInstalledPackages } from "../utilities/TarballMaker.js"
3
+ import { testEspeakSynthesisWithPrePhonemizedInputs, testKirshenbaumPhonemization } from "../synthesis/EspeakTTS.js"
4
+
5
+ const log = logToStderr
6
+
7
+ setupProgramTerminationListeners()
8
+ //process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
9
+ //process.env.http_proxy = "http://localhost:8080"
10
+
11
+ //const testText = `After a while, finding that nothing more happened, she decided on going into the garden at once; but, alas for poor Alice! when she got to the door, she found she had forgotten the little golden key, and when she went back to the table for it, she found she could not possibly reach it: she could see it quite plainly through the glass, and she tried her best to climb up one of the legs of the table, but it was too slippery; and when she had tired herself out with trying, the poor little thing sat down and cried.`
12
+
13
+ //await testKirshenbaumPhonemization(testText)
14
+
15
+ //await makeTarballsForInstalledPackages(true)
16
+
17
+ //getRepetitionScoreRelativeToFirstSubstring(['a', 'b', 'c', 'a', 'b', 'c'])
18
+ //getRepetitionScoreRelativeToFirstSubstring(['a', 'b', 'a', 'd', 'a', 'b', 'a', 'd'])
19
+ //getRepetitionScoreRelativeToFirstSubstring(['a', 'b', 'a', 'b', 'c', 'a', 'b', 'a', 'b'])
20
+ //getRepetitionScoreRelativeToFirstSubstring(['a', 'a', 'a', 'b', 'a', 'a', 'a', 'b'])
21
+ //getRepetitionScoreRelativeToFirstSubstring(['a', 'b', 'a', 'c', 'a', 'b', 'a', 'c', 'a'])
22
+ //getRepetitionScoreRelativeToFirstSubstring(['a', 'a', 'a', 'b', 'b', 'a', 'a', 'a', 'b'])
23
+
24
+ process.exit(0)
@@ -0,0 +1,53 @@
1
+ import path from 'path'
2
+ import type { LanguageDetectionResults } from '../api/LanguageDetection.js'
3
+ import { languageCodeToName } from '../utilities/Locale.js'
4
+ import { OpenPromise } from '../utilities/OpenPromise.js'
5
+ import { resolveModuleMainPath, roundToDigits } from '../utilities/Utilities.js'
6
+
7
+ let fastTextLanguageDetectionModel: any
8
+
9
+ export async function detectLanguage(text: string) {
10
+ const model = await loadLanguageDetectionModel()
11
+
12
+ const predictionsVector = model.predict(text, -1, 0.0)
13
+
14
+ const predictions: LanguageDetectionResults = []
15
+
16
+ for (let i = 0; i < predictionsVector.size(); i++) {
17
+ const prediction = predictionsVector.get(i)
18
+ const languageCode = prediction[1].substring(9)
19
+ const probability = roundToDigits(prediction[0], 3)
20
+
21
+ predictions.push({
22
+ language: languageCode,
23
+ languageName: languageCodeToName(languageCode),
24
+ probability
25
+ })
26
+ }
27
+
28
+ return predictions
29
+ }
30
+
31
+ async function loadLanguageDetectionModel() {
32
+ if (fastTextLanguageDetectionModel) {
33
+ return fastTextLanguageDetectionModel
34
+ }
35
+
36
+ const { FastText, addOnPostRun } = await import('@echogarden/fasttext-wasm')
37
+
38
+ const resultPromise = new OpenPromise<any>()
39
+
40
+ addOnPostRun(async () => {
41
+ const fastText = new FastText()
42
+
43
+ const moduleMainPath = await resolveModuleMainPath('@echogarden/fasttext-wasm')
44
+ const modelPath = path.join(path.dirname(moduleMainPath), 'lid.176.ftz')
45
+
46
+ const model = await fastText.loadModel(modelPath)
47
+
48
+ fastTextLanguageDetectionModel = model
49
+ resultPromise.resolve(model)
50
+ })
51
+
52
+ return resultPromise.promise
53
+ }
@@ -0,0 +1,16 @@
1
+ import type { LanguageDetectionResults } from '../api/LanguageDetection.js'
2
+ import { detectAll } from 'tinyld'
3
+ import { languageCodeToName } from '../utilities/Locale.js'
4
+
5
+ export async function detectLanguage(text: string) {
6
+ const tinyldResults = detectAll(text)
7
+
8
+ const results: LanguageDetectionResults =
9
+ tinyldResults.map(result => ({
10
+ language: result.lang,
11
+ languageName: languageCodeToName(result.lang),
12
+ probability: result.accuracy
13
+ }))
14
+
15
+ return results
16
+ }
@@ -0,0 +1,41 @@
1
+ declare module 'moving-median'
2
+ declare module 'html-to-text'
3
+ declare module 'cldr-segmentation'
4
+
5
+ declare module 'html-escaper' {
6
+ export function escape(str: string): string
7
+ export function unescape(str: string): string
8
+ }
9
+
10
+ declare module 'sam-js'
11
+ declare module 'winax'
12
+
13
+ declare module 'tinyld' {
14
+ export declare function detect(text: string, opts?: any): string
15
+
16
+ export declare function detectAll(text: string, opts?: any): {
17
+ lang: string
18
+ accuracy: number
19
+ }[]
20
+ }
21
+
22
+ declare module 'command-exists' {
23
+ function commandExists(commandName: string): Promise<string>
24
+
25
+ export default commandExists
26
+ }
27
+
28
+ declare module 'wtf_wikipedia'
29
+
30
+ declare module 'kuromoji'
31
+
32
+ declare module '@echogarden/espeak-ng-emscripten'
33
+ declare module '@echogarden/svoxpico-wasm'
34
+ declare module '@echogarden/fasttext-wasm'
35
+ declare module '@echogarden/rubberband-wasm'
36
+ declare module '@echogarden/rnnoise-wasm'
37
+ declare module '@echogarden/fvad-wasm'
38
+ declare module '@echogarden/sonic-wasm'
39
+ declare module '@echogarden/kissfft-wasm'
40
+ declare module '@echogarden/speex-resampler-wasm'
41
+ declare module '@echogarden/vosk'
@@ -0,0 +1,159 @@
1
+ // Typed arrays to Buffer (little endian) conversions
2
+
3
+ // int8 <-> bufferLE
4
+ export function int8ToBuffer(ints: Int8Array) {
5
+ const buffer = Buffer.alloc(ints.length)
6
+
7
+ for (let i = 0; i < ints.length; i++) {
8
+ buffer[i] = ints[i] + 128
9
+ }
10
+
11
+ return buffer
12
+ }
13
+
14
+ export function bufferToInt8(buffer: Buffer) {
15
+ const result = new Int8Array(buffer.length)
16
+
17
+ for (let i = 0; i < result.length; i++) {
18
+ result[i] = buffer[i] - 128
19
+ }
20
+
21
+ return result
22
+ }
23
+
24
+ // int16 <-> bufferLE
25
+ export function int16ToBufferLE(ints: Int16Array) {
26
+ const buffer = Buffer.alloc(ints.length * 2)
27
+
28
+ for (let i = 0; i < ints.length; i++) {
29
+ buffer.writeInt16LE(ints[i], i * 2)
30
+ }
31
+
32
+ return buffer
33
+ }
34
+
35
+ export function bufferLEToInt16(buffer: Buffer) {
36
+ const result = new Int16Array(buffer.length / 2)
37
+
38
+ for (let i = 0; i < result.length; i++) {
39
+ result[i] = buffer.readInt16LE(i * 2)
40
+ }
41
+
42
+ return result
43
+ }
44
+
45
+ // int24 <-> bufferLE (uses int32 for storage)
46
+ export function int24ToBufferLE(ints: Int32Array) {
47
+ const buffer = Buffer.alloc(ints.length * 3)
48
+
49
+ for (let i = 0; i < ints.length; i++) {
50
+ const val = ints[i]
51
+ const encodedVal = val < 0 ? val + 0x1000000 : val
52
+
53
+ buffer[(i * 3) + 0] = (encodedVal >> 0) & 0xff
54
+ buffer[(i * 3) + 1] = (encodedVal >> 8) & 0xff
55
+ buffer[(i * 3) + 2] = (encodedVal >> 16) & 0xff
56
+ }
57
+
58
+ return buffer
59
+ }
60
+
61
+ export function bufferLEToInt24(buffer: Buffer) {
62
+ const result = new Int32Array(buffer.length / 3)
63
+
64
+ for (let i = 0; i < result.length; i++) {
65
+ const b0 = buffer[(i * 3) + 0]
66
+ const b1 = buffer[(i * 3) + 1]
67
+ const b2 = buffer[(i * 3) + 2]
68
+
69
+ const encodedVal = (b0 << 0) + (b1 << 8) + (b2 << 16)
70
+ result[i] = encodedVal > 0x800000 ? encodedVal - 0x1000000 : encodedVal
71
+ }
72
+
73
+ return result
74
+ }
75
+
76
+ // int32 <-> bufferLE
77
+ export function int32ToBufferLE(ints: Int32Array) {
78
+ const buffer = Buffer.alloc(ints.length * 4)
79
+
80
+ for (let i = 0; i < ints.length; i++) {
81
+ buffer.writeInt32LE(ints[i], i * 4)
82
+ }
83
+
84
+ return buffer
85
+ }
86
+
87
+ export function bufferLEToInt32(buffer: Buffer) {
88
+ const result = new Int32Array(buffer.length / 4)
89
+
90
+ for (let i = 0; i < result.length; i++) {
91
+ result[i] = buffer.readInt32LE(i * 4)
92
+ }
93
+
94
+ return result
95
+ }
96
+
97
+ // float32 <-> bufferLE
98
+ export function float32ToBufferLE(floats: Float32Array) {
99
+ const buffer = Buffer.alloc(floats.length * 4)
100
+
101
+ for (let i = 0; i < floats.length; i++) {
102
+ buffer.writeFloatLE(floats[i], i * 4)
103
+ }
104
+
105
+ return buffer
106
+ }
107
+
108
+ export function bufferLEToFloat32(buffer: Buffer) {
109
+ const result = new Float32Array(buffer.length / 4)
110
+
111
+ for (let i = 0; i < result.length; i++) {
112
+ result[i] = buffer.readFloatLE(i * 4)
113
+ }
114
+
115
+ return result
116
+ }
117
+
118
+ // float64 <-> bufferLE
119
+ export function float64ToBufferLE(floats: Float64Array) {
120
+ const buffer = Buffer.alloc(floats.length * 8)
121
+
122
+ for (let i = 0; i < floats.length; i++) {
123
+ buffer.writeDoubleLE(floats[i], i * 8)
124
+ }
125
+
126
+ return buffer
127
+ }
128
+
129
+ export function bufferLEToFloat64(buffer: Buffer) {
130
+ const result = new Float64Array(buffer.length / 8)
131
+
132
+ for (let i = 0; i < result.length; i++) {
133
+ result[i] = buffer.readDoubleLE(i * 8)
134
+ }
135
+
136
+ return result
137
+ }
138
+
139
+ // float64 <-> float32
140
+ export function float64Tofloat32(doubles: Float64Array) {
141
+ const floats = new Float32Array(doubles.length)
142
+
143
+ for (let i = 0; i < doubles.length; i++) {
144
+ floats[i] = doubles[i]
145
+ }
146
+
147
+ return floats
148
+ }
149
+
150
+ export function float32Tofloat64(floats: Float32Array) {
151
+ const doubles = new Float64Array(floats.length)
152
+
153
+ for (let i = 0; i < floats.length; i++) {
154
+ doubles[i] = floats[i]
155
+ }
156
+
157
+ return doubles
158
+ }
159
+
@@ -0,0 +1,91 @@
1
+ import path from 'node:path'
2
+ import { Logger } from './Logger.js'
3
+ import { readdir, stat } from './FileSystem.js'
4
+
5
+ export async function createTarball(filePath: string, outputFile: string, prefixPath = "") {
6
+ const pathStat = await stat(filePath)
7
+
8
+ if (pathStat.isDirectory()) {
9
+ await createTarballForDir(filePath, outputFile, prefixPath)
10
+ } else {
11
+ await createTarballForFile(filePath, outputFile, prefixPath)
12
+ }
13
+ }
14
+
15
+ export async function createTarballForFile(filePath: string, outputFile: string, prefixPath = "") {
16
+ const logger = new Logger()
17
+
18
+ logger.start(`Creating ${prefixPath || path.basename(outputFile)}`)
19
+
20
+ const { default: tar } = await import('tar')
21
+
22
+ const inputFileStat = await stat(filePath)
23
+
24
+ if (!inputFileStat.isFile()) {
25
+ throw new Error(`${filePath} is not a file`)
26
+ }
27
+
28
+ const filename = path.basename(filePath)
29
+ const dirname = path.dirname(filePath)
30
+
31
+ await tar.create({
32
+ gzip: { level: 9, memLevel: 9 },
33
+ file: outputFile,
34
+ cwd: dirname,
35
+ prefix: prefixPath,
36
+ mode: 0o775,
37
+
38
+ filter: (path, stat) => {
39
+ stat.mode |= 0o775
40
+
41
+ return true
42
+ }
43
+
44
+ }, [filename])
45
+
46
+ logger.end()
47
+ }
48
+
49
+ export async function createTarballForDir(inputDir: string, outputFile: string, prefixPath = "") {
50
+ const logger = new Logger()
51
+
52
+ logger.start(`Creating ${prefixPath || path.basename(outputFile)}`)
53
+
54
+ const { default: tar } = await import('tar')
55
+
56
+ const inputDirStat = await stat(inputDir)
57
+
58
+ if (!inputDirStat.isDirectory()) {
59
+ throw new Error(`${inputDir} is not a directory`)
60
+ }
61
+
62
+ const filesInBaseDir = await readdir(inputDir)
63
+
64
+ await tar.create({
65
+ gzip: { level: 9, memLevel: 9 },
66
+ file: outputFile,
67
+ cwd: inputDir,
68
+ prefix: prefixPath,
69
+ mode: 0o775,
70
+
71
+ filter: (path, stat) => {
72
+ stat.mode |= 0o775
73
+
74
+ return true
75
+ }
76
+
77
+ }, filesInBaseDir)
78
+
79
+ logger.end()
80
+ }
81
+
82
+ export async function extractTarball(filepath: string, outputDir: string) {
83
+ const { default: tar } = await import('tar')
84
+
85
+ await tar.extract({
86
+ file: filepath,
87
+ cwd: outputDir,
88
+ preserveOwner: false,
89
+ //noChmod: true
90
+ })
91
+ }
@@ -0,0 +1,201 @@
1
+ import { GaxiosOptions, request } from "gaxios"
2
+ import { Readable } from "stream"
3
+ import { getRandomHexString, writeToStderr } from "./Utilities.js"
4
+ import { OpenPromise } from "./OpenPromise.js"
5
+
6
+ import { Timer } from "./Timer.js"
7
+ import { Logger } from "./Logger.js"
8
+ import path from "node:path"
9
+ import { extractTarball } from "./Compression.js"
10
+ import { createWriteStream, move, remove, readdir, ensureDir } from "./FileSystem.js"
11
+ import chalk from "chalk"
12
+
13
+ export async function downloadAndExtractTarball(options: GaxiosOptions, targetDir: string, baseTempPath: string, displayName = "archive") {
14
+ const logger = new Logger()
15
+
16
+ const randomID = getRandomHexString(16).toLowerCase()
17
+ const tempTarballPath = path.join(baseTempPath, `/${randomID}.tarball`)
18
+ const tempDirPath = path.join(baseTempPath, `/${randomID}`)
19
+ await ensureDir(tempDirPath)
20
+
21
+ await downloadFile(options, tempTarballPath, `${chalk.cyanBright('Downloading')} ${chalk.greenBright(displayName)}`)
22
+ logger.end()
23
+
24
+ logger.start(`Extracting ${displayName}`)
25
+
26
+ await extractTarball(tempTarballPath, tempDirPath)
27
+
28
+ await remove(tempTarballPath)
29
+
30
+ for (const filename of await readdir(tempDirPath)) {
31
+ const sourceFilePath = path.join(tempDirPath, filename)
32
+ const targetFilePath = path.join(targetDir, filename)
33
+
34
+ await move(sourceFilePath, targetFilePath)
35
+ }
36
+
37
+ await remove(tempDirPath)
38
+
39
+ logger.end()
40
+ }
41
+
42
+ export async function downloadFile(options: GaxiosOptions, targetFilePath: string, prompt = "Downloading") {
43
+ const write = writeToStderr
44
+
45
+ const downloadPromise = new OpenPromise<void>()
46
+
47
+ const timer = new Timer()
48
+
49
+ options.responseType = 'stream'
50
+
51
+ const response = await request<Readable>(options)
52
+
53
+ const ttyOutput = process.stderr.isTTY === true
54
+
55
+ write(`\n${prompt}.. `)
56
+
57
+ const rateAveragingWindowSeconds = 5.0
58
+
59
+ let downloadStarted = false
60
+ let downloadedBytes = 0
61
+ let totalBytes: number | undefined = undefined
62
+
63
+ const statusUpdateInterval = 250
64
+
65
+ let lastString = prompt
66
+
67
+ const downloadStateHistory: { time: number, downloadedMBytes: number }[] = []
68
+
69
+ function updateStatus() {
70
+ if (!downloadStarted) {
71
+ return
72
+ }
73
+
74
+ const totalMBytes = (totalBytes || 0) / 1000 / 1000
75
+ const downloadedMBytes = downloadedBytes / 1000 / 1000
76
+
77
+ const elapsedTime = timer.elapsedTimeSeconds
78
+ const cumulativeDownloadRate = downloadedMBytes / elapsedTime
79
+
80
+ const windowStartRecord = downloadStateHistory.find(r => r.time >= timer.elapsedTimeSeconds - rateAveragingWindowSeconds)
81
+
82
+ let windowDownloadRate: number
83
+
84
+ if (windowStartRecord) {
85
+ windowDownloadRate = (downloadedMBytes - windowStartRecord.downloadedMBytes) / (elapsedTime - windowStartRecord.time)
86
+ } else {
87
+ windowDownloadRate = cumulativeDownloadRate
88
+ }
89
+
90
+ downloadStateHistory.push({ time: elapsedTime, downloadedMBytes })
91
+
92
+ const isLastUpdate = downloadedMBytes == totalMBytes
93
+
94
+ const downloadedMbytesStr = downloadedMBytes.toFixed(2)
95
+ const totalMbytesStr = totalMBytes.toFixed(2)
96
+ const downloadRateStr = windowDownloadRate.toFixed(2)
97
+ const cumulativeDownloadRateStr = cumulativeDownloadRate.toFixed(2)
98
+
99
+ if (ttyOutput) {
100
+ let newString: string
101
+
102
+ if (totalBytes != undefined) {
103
+ const percentage = (downloadedMBytes / totalMBytes) * 100
104
+
105
+ newString = `${prompt}.. ${downloadedMbytesStr}MB/${totalMbytesStr}MB (${chalk.blueBright(percentage.toFixed(1) + '%')}, ${timer.elapsedTimeSeconds.toFixed(1)}s, ${downloadRateStr}MB/s)`
106
+ } else {
107
+ newString = `${prompt}.. ${downloadedMbytesStr}MB (${timer.elapsedTimeSeconds.toFixed(1)}s, ${downloadRateStr}MB/s)`
108
+ }
109
+
110
+ if (newString != lastString) {
111
+ write('\r')
112
+ write(newString)
113
+ }
114
+
115
+ lastString = newString
116
+ } else {
117
+ if (totalBytes == undefined) {
118
+ return
119
+ }
120
+
121
+ const percent = downloadedBytes / totalBytes
122
+ const percentDisplay = `${(Math.floor(percent * 10) * 10).toString()}%`
123
+
124
+ if (lastString == prompt) {
125
+ write(`(${totalMbytesStr}MB): `)
126
+ }
127
+
128
+ if (percentDisplay != lastString) {
129
+ write(percentDisplay)
130
+
131
+ if (percent == 1.0) {
132
+ write(` (${timer.elapsedTimeSeconds.toFixed(2)}s, ${cumulativeDownloadRateStr}MB/s)`)
133
+ } else {
134
+ write(` `)
135
+ }
136
+
137
+ lastString = percentDisplay
138
+ }
139
+ }
140
+ }
141
+
142
+ const partialFilePath = `${targetFilePath}.${getRandomHexString(16)}.partial`
143
+ const fileWriteStream = createWriteStream(partialFilePath, { encoding: "binary", autoClose: true })
144
+
145
+ let statusInterval = setInterval(() => {
146
+ updateStatus()
147
+ }, statusUpdateInterval)
148
+
149
+ response.data.on("data", (chunk: Uint8Array) => {
150
+ try {
151
+ const contentLengthString = response.headers['content-length']
152
+
153
+ totalBytes = contentLengthString != undefined ? parseInt(contentLengthString) : undefined
154
+
155
+ const chunkLength = chunk.length
156
+
157
+ fileWriteStream.write(chunk)
158
+
159
+ downloadedBytes += chunkLength
160
+
161
+ if (downloadStarted == false) {
162
+ downloadStarted = true
163
+ }
164
+ } catch (err) {
165
+ clearInterval(statusInterval)
166
+
167
+ downloadPromise.reject(err)
168
+ }
169
+ })
170
+
171
+ response.data.on("end", async () => {
172
+ try {
173
+ clearInterval(statusInterval)
174
+ updateStatus()
175
+
176
+ fileWriteStream.end()
177
+
178
+ write("\n")
179
+
180
+ await move(partialFilePath, targetFilePath)
181
+
182
+ downloadPromise.resolve()
183
+ } catch (err) {
184
+ clearInterval(statusInterval)
185
+ downloadPromise.reject(err)
186
+ }
187
+ })
188
+
189
+ response.data.on("error", async (err: any) => {
190
+ try {
191
+ clearInterval(statusInterval)
192
+
193
+ fileWriteStream.end()
194
+ await remove(partialFilePath)
195
+ } finally {
196
+ downloadPromise.reject(err)
197
+ }
198
+ })
199
+
200
+ return downloadPromise.promise
201
+ }