@ai-coustics/aic-sdk 0.6.3
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.
- package/LICENSE +201 -0
- package/LICENSE.AIC-SDK +74 -0
- package/README.md +107 -0
- package/binding.gyp +52 -0
- package/dist/index.d.ts +188 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +264 -0
- package/package.json +60 -0
- package/scripts/download-sdk.js +216 -0
- package/src/binding.cc +469 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Node.js bindings for ai-coustics speech enhancement SDK
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.Model = exports.Parameter = exports.ModelType = exports.ErrorCode = void 0;
|
|
8
|
+
exports.getSdkVersion = getSdkVersion;
|
|
9
|
+
// Load the native addon
|
|
10
|
+
const binding = require('../build/Release/aic_binding.node');
|
|
11
|
+
/**
|
|
12
|
+
* Error codes returned by the SDK
|
|
13
|
+
*/
|
|
14
|
+
var ErrorCode;
|
|
15
|
+
(function (ErrorCode) {
|
|
16
|
+
/** Operation completed successfully */
|
|
17
|
+
ErrorCode[ErrorCode["SUCCESS"] = 0] = "SUCCESS";
|
|
18
|
+
/** Required pointer argument was NULL */
|
|
19
|
+
ErrorCode[ErrorCode["NULL_POINTER"] = 1] = "NULL_POINTER";
|
|
20
|
+
/** License key format is invalid or corrupted */
|
|
21
|
+
ErrorCode[ErrorCode["LICENSE_INVALID"] = 2] = "LICENSE_INVALID";
|
|
22
|
+
/** License key has expired */
|
|
23
|
+
ErrorCode[ErrorCode["LICENSE_EXPIRED"] = 3] = "LICENSE_EXPIRED";
|
|
24
|
+
/** Audio configuration is not supported by the model */
|
|
25
|
+
ErrorCode[ErrorCode["UNSUPPORTED_AUDIO_CONFIG"] = 4] = "UNSUPPORTED_AUDIO_CONFIG";
|
|
26
|
+
/** Process was called with a different audio buffer configuration than initialized */
|
|
27
|
+
ErrorCode[ErrorCode["AUDIO_CONFIG_MISMATCH"] = 5] = "AUDIO_CONFIG_MISMATCH";
|
|
28
|
+
/** Model must be initialized before this operation */
|
|
29
|
+
ErrorCode[ErrorCode["NOT_INITIALIZED"] = 6] = "NOT_INITIALIZED";
|
|
30
|
+
/** Parameter value is outside acceptable range */
|
|
31
|
+
ErrorCode[ErrorCode["PARAMETER_OUT_OF_RANGE"] = 7] = "PARAMETER_OUT_OF_RANGE";
|
|
32
|
+
/** SDK activation failed */
|
|
33
|
+
ErrorCode[ErrorCode["SDK_ACTIVATION_ERROR"] = 8] = "SDK_ACTIVATION_ERROR";
|
|
34
|
+
})(ErrorCode || (exports.ErrorCode = ErrorCode = {}));
|
|
35
|
+
/**
|
|
36
|
+
* Available model types for audio enhancement
|
|
37
|
+
*/
|
|
38
|
+
var ModelType;
|
|
39
|
+
(function (ModelType) {
|
|
40
|
+
/** Native sample rate: 48 kHz, Native num frames: 480, Processing latency: 30ms */
|
|
41
|
+
ModelType[ModelType["QUAIL_L48"] = 0] = "QUAIL_L48";
|
|
42
|
+
/** Native sample rate: 16 kHz, Native num frames: 160, Processing latency: 30ms */
|
|
43
|
+
ModelType[ModelType["QUAIL_L16"] = 1] = "QUAIL_L16";
|
|
44
|
+
/** Native sample rate: 8 kHz, Native num frames: 80, Processing latency: 30ms */
|
|
45
|
+
ModelType[ModelType["QUAIL_L8"] = 2] = "QUAIL_L8";
|
|
46
|
+
/** Native sample rate: 48 kHz, Native num frames: 480, Processing latency: 30ms */
|
|
47
|
+
ModelType[ModelType["QUAIL_S48"] = 3] = "QUAIL_S48";
|
|
48
|
+
/** Native sample rate: 16 kHz, Native num frames: 160, Processing latency: 30ms */
|
|
49
|
+
ModelType[ModelType["QUAIL_S16"] = 4] = "QUAIL_S16";
|
|
50
|
+
/** Native sample rate: 8 kHz, Native num frames: 80, Processing latency: 30ms */
|
|
51
|
+
ModelType[ModelType["QUAIL_S8"] = 5] = "QUAIL_S8";
|
|
52
|
+
/** Native sample rate: 48 kHz, Native num frames: 480, Processing latency: 10ms */
|
|
53
|
+
ModelType[ModelType["QUAIL_XS"] = 6] = "QUAIL_XS";
|
|
54
|
+
/** Native sample rate: 48 kHz, Native num frames: 480, Processing latency: 10ms */
|
|
55
|
+
ModelType[ModelType["QUAIL_XXS"] = 7] = "QUAIL_XXS";
|
|
56
|
+
})(ModelType || (exports.ModelType = ModelType = {}));
|
|
57
|
+
/**
|
|
58
|
+
* Configurable parameters for audio enhancement
|
|
59
|
+
*/
|
|
60
|
+
var Parameter;
|
|
61
|
+
(function (Parameter) {
|
|
62
|
+
/**
|
|
63
|
+
* Controls the intensity of speech enhancement processing.
|
|
64
|
+
* Range: 0.0 to 1.0
|
|
65
|
+
* - 0.0: Bypass mode - original signal passes through unchanged
|
|
66
|
+
* - 1.0: Full enhancement - maximum noise reduction
|
|
67
|
+
* Default: 1.0
|
|
68
|
+
*/
|
|
69
|
+
Parameter[Parameter["ENHANCEMENT_LEVEL"] = 0] = "ENHANCEMENT_LEVEL";
|
|
70
|
+
/**
|
|
71
|
+
* Compensates for perceived volume reduction after noise removal.
|
|
72
|
+
* Range: 0.1 to 4.0 (linear amplitude multiplier)
|
|
73
|
+
* - 0.1: Significant volume reduction (-20 dB)
|
|
74
|
+
* - 1.0: No gain change (0 dB, default)
|
|
75
|
+
* - 2.0: Double amplitude (+6 dB)
|
|
76
|
+
* - 4.0: Maximum boost (+12 dB)
|
|
77
|
+
* Default: 1.0
|
|
78
|
+
*/
|
|
79
|
+
Parameter[Parameter["VOICE_GAIN"] = 1] = "VOICE_GAIN";
|
|
80
|
+
/**
|
|
81
|
+
* Enables/disables a noise gate as a post-processing step.
|
|
82
|
+
* Valid values: 0.0 or 1.0
|
|
83
|
+
* - 0.0: Noise gate disabled
|
|
84
|
+
* - 1.0: Noise gate enabled
|
|
85
|
+
* Default: 0.0
|
|
86
|
+
*/
|
|
87
|
+
Parameter[Parameter["NOISE_GATE_ENABLE"] = 2] = "NOISE_GATE_ENABLE";
|
|
88
|
+
})(Parameter || (exports.Parameter = Parameter = {}));
|
|
89
|
+
/**
|
|
90
|
+
* High-level interface for the ai-coustics speech enhancement model
|
|
91
|
+
*/
|
|
92
|
+
class Model {
|
|
93
|
+
/**
|
|
94
|
+
* Creates a new audio enhancement model instance
|
|
95
|
+
* @param modelType - The enhancement algorithm variant to use
|
|
96
|
+
* @param licenseKey - Your license key
|
|
97
|
+
* @throws Error if model creation fails
|
|
98
|
+
*/
|
|
99
|
+
constructor(modelType, licenseKey) {
|
|
100
|
+
this._isInitialized = false;
|
|
101
|
+
const result = binding.createModel(modelType, licenseKey);
|
|
102
|
+
if (result.error !== ErrorCode.SUCCESS) {
|
|
103
|
+
throw new Error(`Failed to create model: ${this.getErrorMessage(result.error)}`);
|
|
104
|
+
}
|
|
105
|
+
this.handle = result.model;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Configures the model for a specific audio format
|
|
109
|
+
* Must be called before processing any audio
|
|
110
|
+
* @param config - Audio configuration parameters
|
|
111
|
+
* @throws Error if initialization fails
|
|
112
|
+
*/
|
|
113
|
+
initialize(config) {
|
|
114
|
+
const error = binding.initialize(this.handle, config.sampleRate, config.numChannels, config.numFrames);
|
|
115
|
+
if (error !== ErrorCode.SUCCESS) {
|
|
116
|
+
throw new Error(`Failed to initialize model: ${this.getErrorMessage(error)}`);
|
|
117
|
+
}
|
|
118
|
+
this._isInitialized = true;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Clears all internal state and buffers
|
|
122
|
+
* Call this when the audio stream is interrupted
|
|
123
|
+
* @throws Error if reset fails
|
|
124
|
+
*/
|
|
125
|
+
reset() {
|
|
126
|
+
const error = binding.reset(this.handle);
|
|
127
|
+
if (error !== ErrorCode.SUCCESS) {
|
|
128
|
+
throw new Error(`Failed to reset model: ${this.getErrorMessage(error)}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Processes audio with interleaved channel data (in-place)
|
|
133
|
+
* @param audio - Float32Array containing interleaved audio data
|
|
134
|
+
* @param numChannels - Number of channels (must match initialization)
|
|
135
|
+
* @param numFrames - Number of frames (must match initialization)
|
|
136
|
+
* @throws Error if processing fails
|
|
137
|
+
*/
|
|
138
|
+
processInterleaved(audio, numChannels, numFrames) {
|
|
139
|
+
const error = binding.processInterleaved(this.handle, audio, numChannels, numFrames);
|
|
140
|
+
if (error !== ErrorCode.SUCCESS) {
|
|
141
|
+
throw new Error(`Failed to process audio: ${this.getErrorMessage(error)}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Processes audio with separate buffers for each channel (in-place)
|
|
146
|
+
* Maximum of 16 channels
|
|
147
|
+
* @param audio - Array of Float32Array, one per channel
|
|
148
|
+
* @param numChannels - Number of channels (must match initialization)
|
|
149
|
+
* @param numFrames - Number of frames per channel (must match initialization)
|
|
150
|
+
* @throws Error if processing fails
|
|
151
|
+
*/
|
|
152
|
+
processPlanar(audio, numChannels, numFrames) {
|
|
153
|
+
const error = binding.processPlanar(this.handle, audio, numChannels, numFrames);
|
|
154
|
+
if (error !== ErrorCode.SUCCESS) {
|
|
155
|
+
throw new Error(`Failed to process audio: ${this.getErrorMessage(error)}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Sets a model parameter
|
|
160
|
+
* @param parameter - The parameter to modify
|
|
161
|
+
* @param value - New parameter value
|
|
162
|
+
* @throws Error if setting parameter fails
|
|
163
|
+
*/
|
|
164
|
+
setParameter(parameter, value) {
|
|
165
|
+
const error = binding.setParameter(this.handle, parameter, value);
|
|
166
|
+
if (error !== ErrorCode.SUCCESS) {
|
|
167
|
+
throw new Error(`Failed to set parameter: ${this.getErrorMessage(error)}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Gets the current value of a parameter
|
|
172
|
+
* @param parameter - The parameter to query
|
|
173
|
+
* @returns Current parameter value
|
|
174
|
+
* @throws Error if getting parameter fails
|
|
175
|
+
*/
|
|
176
|
+
getParameter(parameter) {
|
|
177
|
+
const result = binding.getParameter(this.handle, parameter);
|
|
178
|
+
if (result.error !== ErrorCode.SUCCESS) {
|
|
179
|
+
throw new Error(`Failed to get parameter: ${this.getErrorMessage(result.error)}`);
|
|
180
|
+
}
|
|
181
|
+
return result.value;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Returns the total output delay in samples for the current audio configuration
|
|
185
|
+
* @returns Delay in samples
|
|
186
|
+
* @throws Error if getting delay fails
|
|
187
|
+
*/
|
|
188
|
+
getOutputDelay() {
|
|
189
|
+
const result = binding.getOutputDelay(this.handle);
|
|
190
|
+
if (result.error !== ErrorCode.SUCCESS) {
|
|
191
|
+
throw new Error(`Failed to get output delay: ${this.getErrorMessage(result.error)}`);
|
|
192
|
+
}
|
|
193
|
+
return result.delay;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Gets the native sample rate of the selected model
|
|
197
|
+
* @returns Optimal sample rate in Hz
|
|
198
|
+
* @throws Error if getting sample rate fails
|
|
199
|
+
*/
|
|
200
|
+
getOptimalSampleRate() {
|
|
201
|
+
const result = binding.getOptimalSampleRate(this.handle);
|
|
202
|
+
if (result.error !== ErrorCode.SUCCESS) {
|
|
203
|
+
throw new Error(`Failed to get optimal sample rate: ${this.getErrorMessage(result.error)}`);
|
|
204
|
+
}
|
|
205
|
+
return result.sampleRate;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Gets the native number of frames for the selected model
|
|
209
|
+
* @returns Optimal frame count
|
|
210
|
+
* @throws Error if getting frame count fails
|
|
211
|
+
*/
|
|
212
|
+
getOptimalNumFrames() {
|
|
213
|
+
const result = binding.getOptimalNumFrames(this.handle);
|
|
214
|
+
if (result.error !== ErrorCode.SUCCESS) {
|
|
215
|
+
throw new Error(`Failed to get optimal num frames: ${this.getErrorMessage(result.error)}`);
|
|
216
|
+
}
|
|
217
|
+
return result.numFrames;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Checks if the model has been initialized
|
|
221
|
+
*/
|
|
222
|
+
get isInitialized() {
|
|
223
|
+
return this._isInitialized;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Releases all resources associated with the model
|
|
227
|
+
*/
|
|
228
|
+
destroy() {
|
|
229
|
+
if (this.handle) {
|
|
230
|
+
binding.destroyModel(this.handle);
|
|
231
|
+
this.handle = null;
|
|
232
|
+
this._isInitialized = false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
getErrorMessage(errorCode) {
|
|
236
|
+
const messages = {
|
|
237
|
+
[ErrorCode.SUCCESS]: 'Operation completed successfully',
|
|
238
|
+
[ErrorCode.NULL_POINTER]: 'Required pointer argument was NULL',
|
|
239
|
+
[ErrorCode.LICENSE_INVALID]: 'License key format is invalid or corrupted',
|
|
240
|
+
[ErrorCode.LICENSE_EXPIRED]: 'License key has expired',
|
|
241
|
+
[ErrorCode.UNSUPPORTED_AUDIO_CONFIG]: 'Audio configuration is not supported by the model',
|
|
242
|
+
[ErrorCode.AUDIO_CONFIG_MISMATCH]: 'Process was called with a different audio buffer configuration',
|
|
243
|
+
[ErrorCode.NOT_INITIALIZED]: 'Model must be initialized before this operation',
|
|
244
|
+
[ErrorCode.PARAMETER_OUT_OF_RANGE]: 'Parameter value is outside acceptable range',
|
|
245
|
+
[ErrorCode.SDK_ACTIVATION_ERROR]: 'SDK activation failed',
|
|
246
|
+
};
|
|
247
|
+
return messages[errorCode] || `Unknown error code: ${errorCode}`;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
exports.Model = Model;
|
|
251
|
+
/**
|
|
252
|
+
* Returns the version of the SDK
|
|
253
|
+
*/
|
|
254
|
+
function getSdkVersion() {
|
|
255
|
+
return binding.getSdkVersion();
|
|
256
|
+
}
|
|
257
|
+
// Export for CommonJS compatibility
|
|
258
|
+
exports.default = {
|
|
259
|
+
Model,
|
|
260
|
+
ModelType,
|
|
261
|
+
Parameter,
|
|
262
|
+
ErrorCode,
|
|
263
|
+
getSdkVersion,
|
|
264
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ai-coustics/aic-sdk",
|
|
3
|
+
"version": "0.6.3",
|
|
4
|
+
"description": "Node.js bindings for ai-coustics speech enhancement SDK",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"install": "node scripts/download-sdk.js && node-gyp rebuild",
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"prepare": "npm run build",
|
|
11
|
+
"test": "node test-basic.js",
|
|
12
|
+
"test:full": "node test-basic.js && npm run example:js && npm run example:ts",
|
|
13
|
+
"example:js": "node examples/example.js",
|
|
14
|
+
"example:ts": "npx ts-node examples/example.ts",
|
|
15
|
+
"example:ts-compile": "tsc examples/example.ts --outDir examples && node examples/example.js",
|
|
16
|
+
"clean": "rm -rf build dist sdk"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"audio",
|
|
20
|
+
"speech",
|
|
21
|
+
"enhancement",
|
|
22
|
+
"voice",
|
|
23
|
+
"ai-coustics"
|
|
24
|
+
],
|
|
25
|
+
"author": "ai-coustics GmbH",
|
|
26
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/ai-coustics/aic-sdk-node.git"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=14.0.0"
|
|
33
|
+
},
|
|
34
|
+
"os": [
|
|
35
|
+
"darwin",
|
|
36
|
+
"linux",
|
|
37
|
+
"win32"
|
|
38
|
+
],
|
|
39
|
+
"cpu": [
|
|
40
|
+
"x64",
|
|
41
|
+
"arm64"
|
|
42
|
+
],
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"node-addon-api": "^7.0.0",
|
|
45
|
+
"node-gyp": "^10.0.0",
|
|
46
|
+
"tar": "^6.2.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^20.0.0",
|
|
50
|
+
"ts-node": "^10.9.2",
|
|
51
|
+
"typescript": "^5.0.0"
|
|
52
|
+
},
|
|
53
|
+
"files": [
|
|
54
|
+
"dist",
|
|
55
|
+
"binding.gyp",
|
|
56
|
+
"scripts",
|
|
57
|
+
"src/binding.cc"
|
|
58
|
+
],
|
|
59
|
+
"gypfile": true
|
|
60
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const https = require("https");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const crypto = require("crypto");
|
|
7
|
+
const { pipeline } = require("stream");
|
|
8
|
+
const { promisify } = require("util");
|
|
9
|
+
const tar = require("tar");
|
|
10
|
+
const { exec } = require("child_process");
|
|
11
|
+
|
|
12
|
+
const pipelineAsync = promisify(pipeline);
|
|
13
|
+
const execAsync = promisify(exec);
|
|
14
|
+
|
|
15
|
+
const SDK_VERSION = "0.6.3";
|
|
16
|
+
const SDK_BASE_URL = `https://github.com/ai-coustics/aic-sdk-c/releases/download/${SDK_VERSION}`;
|
|
17
|
+
|
|
18
|
+
// Platform-specific archive filenames
|
|
19
|
+
const PLATFORM_MAP = {
|
|
20
|
+
"darwin-x64": `aic-sdk-x86_64-apple-darwin-${SDK_VERSION}.tar.gz`,
|
|
21
|
+
"darwin-arm64": `aic-sdk-aarch64-apple-darwin-${SDK_VERSION}.tar.gz`,
|
|
22
|
+
"linux-x64": `aic-sdk-x86_64-unknown-linux-gnu-${SDK_VERSION}.tar.gz`,
|
|
23
|
+
"linux-arm64": `aic-sdk-aarch64-unknown-linux-gnu-${SDK_VERSION}.tar.gz`,
|
|
24
|
+
"win32-x64": `aic-sdk-x86_64-pc-windows-msvc-${SDK_VERSION}.zip`,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// SHA-256 hashes for each platform archive
|
|
28
|
+
const PLATFORM_HASHES = {
|
|
29
|
+
"darwin-x64":
|
|
30
|
+
"a1e8050c8b87b645c2acb5bce396aa964640074b183da54248c2ef9549c41b6b",
|
|
31
|
+
"darwin-arm64":
|
|
32
|
+
"35384d0e51733f39276c427a92f13d4a983404634604ec5fbeda10a3debc2860",
|
|
33
|
+
"linux-x64":
|
|
34
|
+
"e22593f5cc6241be3d495d4a154c1157f298213e614cbe248a419745fc02e681",
|
|
35
|
+
"linux-arm64":
|
|
36
|
+
"3c10d6af456d8d6641f7e0f82e85145f79d7b1b6459c820e489f685296fafc28",
|
|
37
|
+
"win32-x64":
|
|
38
|
+
"c6a414e23285e3c2930cae4c942f02aea30175a2986a2871304e6229b83bc91b",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function getPlatformIdentifier() {
|
|
42
|
+
const platform = process.platform;
|
|
43
|
+
const arch = process.arch;
|
|
44
|
+
return `${platform}-${arch}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getDownloadUrl() {
|
|
48
|
+
const platformId = getPlatformIdentifier();
|
|
49
|
+
const filename = PLATFORM_MAP[platformId];
|
|
50
|
+
|
|
51
|
+
if (!filename) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Unsupported platform: ${platformId}. Supported platforms: ${Object.keys(PLATFORM_MAP).join(", ")}`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return `${SDK_BASE_URL}/${filename}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function calculateSHA256(filePath) {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const hash = crypto.createHash("sha256");
|
|
63
|
+
const stream = fs.createReadStream(filePath);
|
|
64
|
+
|
|
65
|
+
stream.on("data", (data) => hash.update(data));
|
|
66
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
67
|
+
stream.on("error", reject);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function verifyHash(filePath, expectedHash) {
|
|
72
|
+
return calculateSHA256(filePath).then((actualHash) => {
|
|
73
|
+
if (actualHash !== expectedHash) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Hash verification failed!\nExpected: ${expectedHash}\nActual: ${actualHash}`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
console.log("Hash verification successful!");
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function downloadFile(url, destPath) {
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
console.log(`Downloading SDK from ${url}...`);
|
|
85
|
+
|
|
86
|
+
https
|
|
87
|
+
.get(url, (response) => {
|
|
88
|
+
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
89
|
+
// Follow redirect
|
|
90
|
+
return downloadFile(response.headers.location, destPath)
|
|
91
|
+
.then(resolve)
|
|
92
|
+
.catch(reject);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (response.statusCode !== 200) {
|
|
96
|
+
reject(
|
|
97
|
+
new Error(
|
|
98
|
+
`Failed to download: ${response.statusCode} ${response.statusMessage}`,
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const fileStream = fs.createWriteStream(destPath);
|
|
105
|
+
let downloadedBytes = 0;
|
|
106
|
+
const totalBytes = parseInt(response.headers["content-length"], 10);
|
|
107
|
+
|
|
108
|
+
response.on("data", (chunk) => {
|
|
109
|
+
downloadedBytes += chunk.length;
|
|
110
|
+
const progress = ((downloadedBytes / totalBytes) * 100).toFixed(1);
|
|
111
|
+
process.stdout.write(`\rDownloading: ${progress}%`);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
response.pipe(fileStream);
|
|
115
|
+
|
|
116
|
+
fileStream.on("finish", () => {
|
|
117
|
+
fileStream.close();
|
|
118
|
+
console.log("\nDownload completed!");
|
|
119
|
+
resolve();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
fileStream.on("error", (err) => {
|
|
123
|
+
fs.unlinkSync(destPath);
|
|
124
|
+
reject(err);
|
|
125
|
+
});
|
|
126
|
+
})
|
|
127
|
+
.on("error", reject);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function extractArchive(archivePath, destDir) {
|
|
132
|
+
console.log("Extracting SDK...");
|
|
133
|
+
|
|
134
|
+
const ext = path.extname(archivePath);
|
|
135
|
+
|
|
136
|
+
if (ext === ".gz") {
|
|
137
|
+
// Extract tar.gz
|
|
138
|
+
await tar.x({
|
|
139
|
+
file: archivePath,
|
|
140
|
+
cwd: destDir,
|
|
141
|
+
});
|
|
142
|
+
} else if (ext === ".zip") {
|
|
143
|
+
// Extract zip (Windows)
|
|
144
|
+
const unzipper = require("child_process").spawn("powershell.exe", [
|
|
145
|
+
"-Command",
|
|
146
|
+
`Expand-Archive -Path "${archivePath}" -DestinationPath "${destDir}" -Force`,
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
await new Promise((resolve, reject) => {
|
|
150
|
+
unzipper.on("close", (code) => {
|
|
151
|
+
if (code === 0) resolve();
|
|
152
|
+
else reject(new Error(`Extraction failed with code ${code}`));
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log("Extraction completed!");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function main() {
|
|
161
|
+
try {
|
|
162
|
+
const rootDir = path.join(__dirname, "..");
|
|
163
|
+
const sdkDir = path.join(rootDir, "sdk");
|
|
164
|
+
const tmpDir = path.join(rootDir, "tmp");
|
|
165
|
+
|
|
166
|
+
// Create directories
|
|
167
|
+
if (!fs.existsSync(tmpDir)) {
|
|
168
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Check if SDK already exists
|
|
172
|
+
if (fs.existsSync(sdkDir)) {
|
|
173
|
+
console.log("SDK already downloaded. Skipping download.");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const downloadUrl = getDownloadUrl();
|
|
178
|
+
const filename = path.basename(downloadUrl);
|
|
179
|
+
const archivePath = path.join(tmpDir, filename);
|
|
180
|
+
|
|
181
|
+
// Download SDK
|
|
182
|
+
await downloadFile(downloadUrl, archivePath);
|
|
183
|
+
|
|
184
|
+
// Verify the downloaded file's hash
|
|
185
|
+
const platformId = getPlatformIdentifier();
|
|
186
|
+
const expectedHash = PLATFORM_HASHES[platformId];
|
|
187
|
+
console.log("Verifying download integrity...");
|
|
188
|
+
await verifyHash(archivePath, expectedHash);
|
|
189
|
+
|
|
190
|
+
// Create sdk directory for extraction
|
|
191
|
+
fs.mkdirSync(sdkDir, { recursive: true });
|
|
192
|
+
|
|
193
|
+
// Extract SDK directly into sdk directory
|
|
194
|
+
await extractArchive(archivePath, sdkDir);
|
|
195
|
+
|
|
196
|
+
// Remove unnecessary directories
|
|
197
|
+
const unnecessaryDirs = ["examples", "docs"];
|
|
198
|
+
for (const dir of unnecessaryDirs) {
|
|
199
|
+
const dirPath = path.join(sdkDir, dir);
|
|
200
|
+
if (fs.existsSync(dirPath)) {
|
|
201
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Clean up
|
|
206
|
+
fs.unlinkSync(archivePath);
|
|
207
|
+
fs.rmdirSync(tmpDir);
|
|
208
|
+
|
|
209
|
+
console.log("SDK installation completed successfully!");
|
|
210
|
+
} catch (error) {
|
|
211
|
+
console.error("Failed to download SDK:", error.message);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
main();
|