@qvac/decoder-audio 0.4.0 → 0.6.0

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/package.json CHANGED
@@ -1,20 +1,29 @@
1
1
  {
2
2
  "name": "@qvac/decoder-audio",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Tether",
7
7
  "type": "commonjs",
8
8
  "main": "index.js",
9
9
  "scripts": {
10
- "test:dts": "tsc index.d.ts --noEmit --esModuleInterop --skipLibCheck",
11
- "test:unit": "brittle-bare test/unit/*.test.js",
12
- "test": "npm run test:unit && npm run test:integration",
10
+ "build": "npm run build:ts",
11
+ "build:ts": "tsc -p tsconfig.build.json",
12
+ "check:generated": "node scripts/check-generated.mjs",
13
+ "typecheck": "tsc --noEmit -p tsconfig.json",
14
+ "test:dts": "tsc --noEmit -p tsconfig.dts.json",
15
+ "test:types": "npm run typecheck && npm run test:dts && npm run check:generated",
16
+ "test:unit": "npm run build:ts && brittle-bare test/unit/*.test.js && npm run test:package",
17
+ "test:package": "node --test test/package/package-contract.test.js",
18
+ "test": "npm run test:types && npm run test:unit && npm run test:integration",
13
19
  "coverage:unit": "brittle-bare --coverage --cov-dir=coverage/unit test/unit/*.test.js && mkdir -p .nyc_output && cp coverage/unit/coverage-final.json .nyc_output/out.json && npx nyc report --reporter=html --report-dir=coverage/html",
14
20
  "coverage": "brittle-bare --coverage --cov-dir=coverage/unit test/unit/*.test.js && mkdir -p .nyc_output && cp coverage/unit/coverage-final.json .nyc_output/out.json && npx nyc report --reporter=html --report-dir=coverage/html",
15
- "lint": "standard \"test/**/*.js\" \"*.js\" \"scripts/**/*.js\"",
16
- "lint:fix": "standard --fix \"test/**/*.js\" \"*.js\" \"scripts/**/*.js\"",
17
- "test:integration": "npm run test:integration:generate && brittle-bare test/integration/*.test.js",
21
+ "lint": "npm run lint:js && npm run lint:ts && npm run test:types",
22
+ "lint:js": "prettier --check \"test/**/*.js\" \"scripts/**/*.js\" && lunte \"test/**/*.js\" \"scripts/**/*.js\"",
23
+ "lint:ts": "eslint \"src/**/*.ts\" --max-warnings=0",
24
+ "lint:ts:fix": "eslint \"src/**/*.ts\" --fix",
25
+ "format": "prettier --write \"test/**/*.js\" \"scripts/**/*.js\"",
26
+ "test:integration": "npm run build:ts && npm run test:integration:generate && brittle-bare test/integration/*.test.js",
18
27
  "test:integration:generate": "brittle -r test/integration/all.js test/integration/*.test.js && npm run test:mobile:generate",
19
28
  "test:mobile:generate": "bare ./scripts/generate-mobile-integration-tests.js",
20
29
  "test:mobile:validate": "node scripts/validate-mobile-tests.js"
@@ -37,23 +46,28 @@
37
46
  },
38
47
  "devDependencies": {
39
48
  "@types/node": "^22.14.1",
49
+ "bare-abort-controller": "^1.1.2",
40
50
  "bare-os": "^3.6.2",
41
- "bare-url": "^2.1.6",
51
+ "bare-process": "^4.2.2",
42
52
  "brittle": "^3.13.1",
53
+ "eslint": "^9.39.4",
54
+ "eslint-import-resolver-typescript": "^4.4.5",
55
+ "eslint-plugin-import": "^2.32.0",
56
+ "lunte": "^1.8.0",
43
57
  "nyc": "^17.1.0",
44
- "standard": "^17.1.2",
45
- "typescript": "^5.3.0"
58
+ "prettier": "^3.6.2",
59
+ "prettier-config-holepunch": "^2.0.0",
60
+ "typescript": "^5.9.3",
61
+ "typescript-eslint": "^8.63.0"
46
62
  },
47
63
  "dependencies": {
48
64
  "@qvac/error": "^0.1.0",
49
- "@qvac/infer-base": "^0.4.0",
65
+ "@qvac/infer-base": "^0.6.0",
50
66
  "@qvac/logging": "^0.1.0",
51
- "bare-assert": "^1.1.0",
52
- "bare-channel": "^5.2.2",
53
67
  "bare-ffmpeg": "^1.0.0-32",
54
68
  "bare-fs": "^4.5.1",
55
69
  "bare-path": "^3.0.0",
56
- "bare-process": "^4.2.2"
70
+ "bare-url": "^2.1.6"
57
71
  },
58
72
  "repository": {
59
73
  "type": "git",
@@ -61,6 +75,6 @@
61
75
  "directory": "packages/decoder-audio"
62
76
  },
63
77
  "bugs": "https://github.com/tetherto/qvac/issues",
64
- "homepage": "https://github.com/tetherto/qvac/tree/main/packages/decoder-audio#readme",
78
+ "homepage": "https://qvac.tether.io",
65
79
  "types": "index.d.ts"
66
80
  }
@@ -4,6 +4,30 @@ const path = require('bare-path')
4
4
  const fs = require('bare-fs')
5
5
  const { pathToFileURL } = require('bare-url')
6
6
 
7
+ // A dlopen failure (or any unhandled error) MUST fail the run, not just get
8
+ // logged: Bare surfaces addon-load failures as an unhandledRejection on the
9
+ // worklet thread. Without a hard exit the run can SIGABRT (ambiguous timeout)
10
+ // or a log-only handler would falsely pass. Catch, record the first failure,
11
+ // and force a non-zero exit on drain.
12
+ let _integrationFatalError = null
13
+ const _bareHost = typeof globalThis !== 'undefined' ? globalThis.Bare : undefined
14
+ if (_bareHost && typeof _bareHost.on === 'function') {
15
+ _bareHost.on('unhandledRejection', (reason) => {
16
+ if (!_integrationFatalError) _integrationFatalError = reason || new Error('unhandledRejection')
17
+ console.error('[integration-runner] Unhandled rejection:', reason instanceof Error ? reason.stack : reason)
18
+ })
19
+ _bareHost.on('uncaughtException', (err) => {
20
+ if (!_integrationFatalError) _integrationFatalError = err || new Error('uncaughtException')
21
+ console.error('[integration-runner] Uncaught exception:', err instanceof Error ? err.stack : err)
22
+ })
23
+ _bareHost.on('beforeExit', () => {
24
+ if (!_integrationFatalError) return
25
+ console.error('[integration-runner] FATAL: failing run due to an earlier unhandled error.')
26
+ if (typeof _bareHost.exit === 'function') _bareHost.exit(1)
27
+ else if (typeof globalThis.process !== 'undefined' && globalThis.process.exit) globalThis.process.exit(1)
28
+ })
29
+ }
30
+
7
31
  async function runIntegrationModule (relativeModulePath, options = {}) {
8
32
  const modulePath = path.join(__dirname, relativeModulePath)
9
33
 
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Creates a stream accumulator that processes incoming audio data in chunks of a specified size.
3
+ * The accumulator maintains a buffer of incoming data and emits chunks when the target size is reached.
4
+ *
5
+ * @param options - Configuration options for the stream accumulator
6
+ * @returns An object with methods to process data and finish the stream
7
+ * @throws {QvacErrorDecoderAudio} If the target buffer size is smaller than the minimum required size
8
+ */
9
+ declare function createStreamAccumulator({ onChunk, onFinish, targetBufferSize, }: createStreamAccumulator.StreamAccumulatorOptions): createStreamAccumulator.StreamAccumulator;
10
+ declare namespace createStreamAccumulator {
11
+ interface StreamAccumulatorOptions {
12
+ /** Called with each full chunk, and with the remainder on `finish()`. */
13
+ onChunk: (chunk: Uint8Array) => void | Promise<void>;
14
+ /** Called once after the final chunk has been emitted. */
15
+ onFinish: () => void | Promise<void>;
16
+ /** Chunk size in bytes. Must be at least 64000. */
17
+ targetBufferSize?: number;
18
+ }
19
+ interface StreamAccumulator {
20
+ processData(data: Buffer): Promise<void>;
21
+ finish(): Promise<void>;
22
+ }
23
+ }
24
+ export = createStreamAccumulator;
@@ -1,64 +1,51 @@
1
- const { QvacErrorDecoderAudio, ERR_CODES } = require('./error')
2
-
3
- const TARGET_SECONDS = 1 // Desired chunk size in seconds
4
- const SAMPLE_RATE = 16000 // PCM audio sample rate
5
- const BYTES_PER_SAMPLE = 4 // PCM S16LE (16-bit)
6
- const TARGET_BUFFER_SIZE = TARGET_SECONDS * SAMPLE_RATE * BYTES_PER_SAMPLE
7
-
1
+ "use strict";
2
+ const error_1 = require("./error");
3
+ const TARGET_SECONDS = 1; // Desired chunk size in seconds
4
+ const SAMPLE_RATE = 16000; // PCM audio sample rate
5
+ const BYTES_PER_SAMPLE = 4; // PCM S16LE (16-bit)
6
+ const TARGET_BUFFER_SIZE = TARGET_SECONDS * SAMPLE_RATE * BYTES_PER_SAMPLE;
8
7
  /**
9
8
  * Creates a stream accumulator that processes incoming audio data in chunks of a specified size.
10
9
  * The accumulator maintains a buffer of incoming data and emits chunks when the target size is reached.
11
10
  *
12
- * @param {Object} options - Configuration options for the stream accumulator
13
- * @param {Function} options.onChunk - Callback function called when a chunk of data is ready to be processed
14
- * @param {Function} options.onFinish - Callback function called when all data has been processed
15
- * @param {number} [options.targetBufferSize=TARGET_BUFFER_SIZE] - Target size for each chunk in bytes
16
- * @returns {Object} An object with methods to process data and finish the stream
17
- * @throws {Error} If the target buffer size is smaller than the minimum required size
11
+ * @param options - Configuration options for the stream accumulator
12
+ * @returns An object with methods to process data and finish the stream
13
+ * @throws {QvacErrorDecoderAudio} If the target buffer size is smaller than the minimum required size
18
14
  */
19
- function createStreamAccumulator ({
20
- onChunk,
21
- onFinish,
22
- targetBufferSize = TARGET_BUFFER_SIZE
23
- }) {
24
- if (targetBufferSize < TARGET_BUFFER_SIZE) {
25
- throw new QvacErrorDecoderAudio({ code: ERR_CODES.BUFFER_SIZE_TOO_SMALL })
26
- }
27
-
28
- let accumulator = Buffer.alloc(0)
29
-
30
- return {
31
- /**
32
- * Process incoming data and emit chunks when target size is reached.
33
- * Accumulates incoming data until it reaches the target buffer size,
34
- * then emits a chunk and continues with the remaining data.
35
- *
36
- * @param {Buffer} data - The incoming data to process
37
- * @returns {Promise<void>} A promise that resolves when the chunk is processed
38
- */
39
- async processData (data) {
40
- accumulator = Buffer.concat([accumulator, data])
41
-
42
- while (accumulator.length >= targetBufferSize) {
43
- const chunk = accumulator.subarray(0, targetBufferSize)
44
- await onChunk(new Uint8Array(chunk))
45
- accumulator = accumulator.subarray(targetBufferSize)
46
- }
47
- },
48
-
49
- /**
50
- * Finish processing and emit any remaining data.
51
- * Processes any remaining data in the accumulator and calls the finish callback.
52
- *
53
- * @returns {Promise<void>} A promise that resolves when all remaining data is processed
54
- */
55
- async finish () {
56
- if (accumulator.length > 0) {
57
- await onChunk(new Uint8Array(accumulator))
58
- }
59
- await onFinish()
15
+ function createStreamAccumulator({ onChunk, onFinish, targetBufferSize = TARGET_BUFFER_SIZE, }) {
16
+ if (targetBufferSize < TARGET_BUFFER_SIZE) {
17
+ throw new error_1.QvacErrorDecoderAudio({ code: error_1.ERR_CODES.BUFFER_SIZE_TOO_SMALL });
60
18
  }
61
- }
19
+ let accumulator = Buffer.alloc(0);
20
+ return {
21
+ /**
22
+ * Process incoming data and emit chunks when target size is reached.
23
+ * Accumulates incoming data until it reaches the target buffer size,
24
+ * then emits a chunk and continues with the remaining data.
25
+ *
26
+ * @param data - The incoming data to process
27
+ * @returns A promise that resolves when the chunk is processed
28
+ */
29
+ async processData(data) {
30
+ accumulator = Buffer.concat([accumulator, data]);
31
+ while (accumulator.length >= targetBufferSize) {
32
+ const chunk = accumulator.subarray(0, targetBufferSize);
33
+ await onChunk(new Uint8Array(chunk));
34
+ accumulator = accumulator.subarray(targetBufferSize);
35
+ }
36
+ },
37
+ /**
38
+ * Finish processing and emit any remaining data.
39
+ * Processes any remaining data in the accumulator and calls the finish callback.
40
+ *
41
+ * @returns A promise that resolves when all remaining data is processed
42
+ */
43
+ async finish() {
44
+ if (accumulator.length > 0) {
45
+ await onChunk(new Uint8Array(accumulator));
46
+ }
47
+ await onFinish();
48
+ },
49
+ };
62
50
  }
63
-
64
- module.exports = createStreamAccumulator
51
+ module.exports = createStreamAccumulator;
@@ -0,0 +1,19 @@
1
+ import QvacError = require("@qvac/error");
2
+ declare const QvacErrorBase: typeof QvacError.QvacErrorBase;
3
+ export declare class QvacErrorDecoderAudio extends QvacErrorBase {
4
+ }
5
+ export declare const ERR_CODES: Readonly<{
6
+ FAILED_TO_LOAD_WEIGHTS: 11001;
7
+ FAILED_TO_ACTIVATE: 11002;
8
+ FAILED_TO_PAUSE: 11003;
9
+ FAILED_TO_CANCEL: 11004;
10
+ FAILED_TO_APPEND: 11005;
11
+ FAILED_TO_GET_STATUS: 11006;
12
+ FAILED_TO_DESTROY: 11007;
13
+ BUFFER_SIZE_TOO_SMALL: 11008;
14
+ UNSUPPORTED_AUDIO_FORMAT: 11009;
15
+ DECODER_NOT_LOADED: 11010;
16
+ STREAM_INDEX_OUT_OF_BOUNDS: 11011;
17
+ JOB_CANCELLED: 11012;
18
+ }>;
19
+ export {};
package/utils/error.js CHANGED
@@ -1,81 +1,77 @@
1
- 'use strict'
2
-
3
- const { QvacErrorBase, addCodes } = require('@qvac/error')
4
- const { name, version } = require('../package.json')
5
-
6
- class QvacErrorDecoderAudio extends QvacErrorBase { }
7
-
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ERR_CODES = exports.QvacErrorDecoderAudio = void 0;
4
+ /* eslint-disable @typescript-eslint/no-require-imports -- @qvac/error exposes a CommonJS export shape. */
5
+ const QvacError = require("@qvac/error");
6
+ /* eslint-enable @typescript-eslint/no-require-imports */
7
+ const { QvacErrorBase, addCodes } = QvacError;
8
+ class QvacErrorDecoderAudio extends QvacErrorBase {
9
+ }
10
+ exports.QvacErrorDecoderAudio = QvacErrorDecoderAudio;
11
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- package metadata is read from the package root at runtime.
12
+ const { name, version } = require("../package.json");
8
13
  // This library has error code range from 11,001 to 12,000
9
- const ERR_CODES = Object.freeze({
10
- FAILED_TO_LOAD_WEIGHTS: 11001,
11
- FAILED_TO_ACTIVATE: 11002,
12
- FAILED_TO_PAUSE: 11003,
13
- FAILED_TO_CANCEL: 11004,
14
- FAILED_TO_APPEND: 11005,
15
- FAILED_TO_GET_STATUS: 11006,
16
- FAILED_TO_DESTROY: 11007,
17
- BUFFER_SIZE_TOO_SMALL: 11008,
18
- UNSUPPORTED_AUDIO_FORMAT: 11009,
19
- DECODER_NOT_LOADED: 11010,
20
- STREAM_INDEX_OUT_OF_BOUNDS: 11011,
21
- JOB_CANCELLED: 11012
22
- })
23
-
14
+ exports.ERR_CODES = Object.freeze({
15
+ FAILED_TO_LOAD_WEIGHTS: 11001,
16
+ FAILED_TO_ACTIVATE: 11002,
17
+ FAILED_TO_PAUSE: 11003,
18
+ FAILED_TO_CANCEL: 11004,
19
+ FAILED_TO_APPEND: 11005,
20
+ FAILED_TO_GET_STATUS: 11006,
21
+ FAILED_TO_DESTROY: 11007,
22
+ BUFFER_SIZE_TOO_SMALL: 11008,
23
+ UNSUPPORTED_AUDIO_FORMAT: 11009,
24
+ DECODER_NOT_LOADED: 11010,
25
+ STREAM_INDEX_OUT_OF_BOUNDS: 11011,
26
+ JOB_CANCELLED: 11012,
27
+ });
24
28
  addCodes({
25
- [ERR_CODES.FAILED_TO_LOAD_WEIGHTS]: {
26
- name: 'FAILED_TO_LOAD_WEIGHTS',
27
- message: (message) => `Failed to load weights, error: ${message}`
28
- },
29
- [ERR_CODES.FAILED_TO_ACTIVATE]: {
30
- name: 'FAILED_TO_ACTIVATE',
31
- message: (message) => `Failed to activate model, error: ${message}`
32
- },
33
- [ERR_CODES.FAILED_TO_PAUSE]: {
34
- name: 'FAILED_TO_PAUSE',
35
- message: (message) => `Failed to pause inference, error: ${message}`
36
- },
37
- [ERR_CODES.FAILED_TO_CANCEL]: {
38
- name: 'FAILED_TO_CANCEL',
39
- message: (message) => `Failed to cancel inference, error: ${message}`
40
- },
41
- [ERR_CODES.FAILED_TO_APPEND]: {
42
- name: 'FAILED_TO_APPEND',
43
- message: (message) => `Failed to append data to processing queue, error: ${message}`
44
- },
45
- [ERR_CODES.FAILED_TO_GET_STATUS]: {
46
- name: 'FAILED_TO_GET_STATUS',
47
- message: (message) => `Failed to get decoder status, error: ${message}`
48
- },
49
- [ERR_CODES.FAILED_TO_DESTROY]: {
50
- name: 'FAILED_TO_DESTROY',
51
- message: (message) => `Failed to destroy instance, error: ${message}`
52
- },
53
- [ERR_CODES.BUFFER_SIZE_TOO_SMALL]: {
54
- name: 'BUFFER_SIZE_TOO_SMALL',
55
- message: 'Target buffer size is too small'
56
- },
57
- [ERR_CODES.UNSUPPORTED_AUDIO_FORMAT]: {
58
- name: 'UNSUPPORTED_AUDIO_FORMAT',
59
- message: (format) => `Unsupported audio format: ${format}`
60
- },
61
- [ERR_CODES.DECODER_NOT_LOADED]: {
62
- name: 'DECODER_NOT_LOADED',
63
- message: 'Decoder not loaded. Call load() first.'
64
- },
65
- [ERR_CODES.STREAM_INDEX_OUT_OF_BOUNDS]: {
66
- name: 'STREAM_INDEX_OUT_OF_BOUNDS',
67
- message: (index) => `Stream index out of bounds: ${index}`
68
- },
69
- [ERR_CODES.JOB_CANCELLED]: {
70
- name: 'JOB_CANCELLED',
71
- message: 'Decoder job cancelled'
72
- }
73
- }, {
74
- name,
75
- version
76
- })
77
-
78
- module.exports = {
79
- ERR_CODES,
80
- QvacErrorDecoderAudio
81
- }
29
+ [exports.ERR_CODES.FAILED_TO_LOAD_WEIGHTS]: {
30
+ name: "FAILED_TO_LOAD_WEIGHTS",
31
+ message: (message) => `Failed to load weights, error: ${message}`,
32
+ },
33
+ [exports.ERR_CODES.FAILED_TO_ACTIVATE]: {
34
+ name: "FAILED_TO_ACTIVATE",
35
+ message: (message) => `Failed to activate model, error: ${message}`,
36
+ },
37
+ [exports.ERR_CODES.FAILED_TO_PAUSE]: {
38
+ name: "FAILED_TO_PAUSE",
39
+ message: (message) => `Failed to pause inference, error: ${message}`,
40
+ },
41
+ [exports.ERR_CODES.FAILED_TO_CANCEL]: {
42
+ name: "FAILED_TO_CANCEL",
43
+ message: (message) => `Failed to cancel inference, error: ${message}`,
44
+ },
45
+ [exports.ERR_CODES.FAILED_TO_APPEND]: {
46
+ name: "FAILED_TO_APPEND",
47
+ message: (message) => `Failed to append data to processing queue, error: ${message}`,
48
+ },
49
+ [exports.ERR_CODES.FAILED_TO_GET_STATUS]: {
50
+ name: "FAILED_TO_GET_STATUS",
51
+ message: (message) => `Failed to get decoder status, error: ${message}`,
52
+ },
53
+ [exports.ERR_CODES.FAILED_TO_DESTROY]: {
54
+ name: "FAILED_TO_DESTROY",
55
+ message: (message) => `Failed to destroy instance, error: ${message}`,
56
+ },
57
+ [exports.ERR_CODES.BUFFER_SIZE_TOO_SMALL]: {
58
+ name: "BUFFER_SIZE_TOO_SMALL",
59
+ message: "Target buffer size is too small",
60
+ },
61
+ [exports.ERR_CODES.UNSUPPORTED_AUDIO_FORMAT]: {
62
+ name: "UNSUPPORTED_AUDIO_FORMAT",
63
+ message: (format) => `Unsupported audio format: ${format}`,
64
+ },
65
+ [exports.ERR_CODES.DECODER_NOT_LOADED]: {
66
+ name: "DECODER_NOT_LOADED",
67
+ message: "Decoder not loaded. Call load() first.",
68
+ },
69
+ [exports.ERR_CODES.STREAM_INDEX_OUT_OF_BOUNDS]: {
70
+ name: "STREAM_INDEX_OUT_OF_BOUNDS",
71
+ message: (index) => `Stream index out of bounds: ${index}`,
72
+ },
73
+ [exports.ERR_CODES.JOB_CANCELLED]: {
74
+ name: "JOB_CANCELLED",
75
+ message: "Decoder job cancelled",
76
+ },
77
+ }, { name, version });