chore: update node_modules with new binary files and dependencies
- Add new binary files for nodemon, onnxruntime-web, and xenova/transformers - Update various JavaScript and TypeScript files in node_modules - Remove unused files and dependencies - Add new test fixtures and documentation files
This commit is contained in:
50
node_modules/@xenova/transformers/src/backends/onnx.js
generated
vendored
Normal file
50
node_modules/@xenova/transformers/src/backends/onnx.js
generated
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @file Handler file for choosing the correct version of ONNX Runtime, based on the environment.
|
||||
* Ideally, we could import the `onnxruntime-web` and `onnxruntime-node` packages only when needed,
|
||||
* but dynamic imports don't seem to work with the current webpack version and/or configuration.
|
||||
* This is possibly due to the experimental nature of top-level await statements.
|
||||
* So, we just import both packages, and use the appropriate one based on the environment:
|
||||
* - When running in node, we use `onnxruntime-node`.
|
||||
* - When running in the browser, we use `onnxruntime-web` (`onnxruntime-node` is not bundled).
|
||||
*
|
||||
* This module is not directly exported, but can be accessed through the environment variables:
|
||||
* ```javascript
|
||||
* import { env } from '@xenova/transformers';
|
||||
* console.log(env.backends.onnx);
|
||||
* ```
|
||||
*
|
||||
* @module backends/onnx
|
||||
*/
|
||||
|
||||
// NOTE: Import order matters here. We need to import `onnxruntime-node` before `onnxruntime-web`.
|
||||
// In either case, we select the default export if it exists, otherwise we use the named export.
|
||||
import * as ONNX_NODE from 'onnxruntime-node';
|
||||
import * as ONNX_WEB from 'onnxruntime-web';
|
||||
|
||||
/** @type {import('onnxruntime-web')} The ONNX runtime module. */
|
||||
export let ONNX;
|
||||
|
||||
export const executionProviders = [
|
||||
// 'webgpu',
|
||||
'wasm'
|
||||
];
|
||||
|
||||
if (typeof process !== 'undefined' && process?.release?.name === 'node') {
|
||||
// Running in a node-like environment.
|
||||
ONNX = ONNX_NODE.default ?? ONNX_NODE;
|
||||
|
||||
// Add `cpu` execution provider, with higher precedence that `wasm`.
|
||||
executionProviders.unshift('cpu');
|
||||
|
||||
} else {
|
||||
// Running in a browser-environment
|
||||
ONNX = ONNX_WEB.default ?? ONNX_WEB;
|
||||
|
||||
// SIMD for WebAssembly does not operate correctly in some recent versions of iOS (16.4.x).
|
||||
// As a temporary fix, we disable it for now.
|
||||
// For more information, see: https://github.com/microsoft/onnxruntime/issues/15644
|
||||
const isIOS = typeof navigator !== 'undefined' && /iP(hone|od|ad).+16_4.+AppleWebKit/.test(navigator.userAgent);
|
||||
if (isIOS) {
|
||||
ONNX.env.wasm.simd = false;
|
||||
}
|
||||
}
|
||||
107
node_modules/@xenova/transformers/src/configs.js
generated
vendored
Normal file
107
node_modules/@xenova/transformers/src/configs.js
generated
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
|
||||
/**
|
||||
* @file Helper module for using model configs. For more information, see the corresponding
|
||||
* [Python documentation](https://huggingface.co/docs/transformers/main/en/model_doc/auto#transformers.AutoConfig).
|
||||
*
|
||||
* **Example:** Load an `AutoConfig`.
|
||||
*
|
||||
* ```javascript
|
||||
* import { AutoConfig } from '@xenova/transformers';
|
||||
* let config = await AutoConfig.from_pretrained('bert-base-uncased');
|
||||
* console.log(config);
|
||||
* // PretrainedConfig {
|
||||
* // "model_type": "bert",
|
||||
* // "is_encoder_decoder": false,
|
||||
* // "architectures": [
|
||||
* // "BertForMaskedLM"
|
||||
* // ],
|
||||
* // "vocab_size": 30522
|
||||
* // "num_attention_heads": 12,
|
||||
* // "num_hidden_layers": 12,
|
||||
* // "hidden_size": 768,
|
||||
* // "max_position_embeddings": 512,
|
||||
* // ...
|
||||
* // }
|
||||
* ```
|
||||
*
|
||||
* @module configs
|
||||
*/
|
||||
|
||||
import {
|
||||
getModelJSON,
|
||||
} from './utils/hub.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('./utils/hub.js').PretrainedOptions} PretrainedOptions
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Loads a config from the specified path.
|
||||
* @param {string} pretrained_model_name_or_path The path to the config directory.
|
||||
* @param {PretrainedOptions} options Additional options for loading the config.
|
||||
* @returns {Promise<Array>} A promise that resolves with information about the loaded config.
|
||||
*/
|
||||
async function loadConfig(pretrained_model_name_or_path, options) {
|
||||
let info = await getModelJSON(pretrained_model_name_or_path, 'config.json', true, options);
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for all configuration classes. For more information, see the corresponding
|
||||
* [Python documentation](https://huggingface.co/docs/transformers/main/en/main_classes/configuration#transformers.PretrainedConfig).
|
||||
*/
|
||||
export class PretrainedConfig {
|
||||
// NOTE: Typo in original
|
||||
|
||||
/**
|
||||
* Create a new PreTrainedTokenizer instance.
|
||||
* @param {Object} configJSON The JSON of the config.
|
||||
*/
|
||||
constructor(configJSON) {
|
||||
this.model_type = null;
|
||||
this.is_encoder_decoder = false;
|
||||
|
||||
Object.assign(this, configJSON);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a pre-trained config from the given `pretrained_model_name_or_path`.
|
||||
*
|
||||
* @param {string} pretrained_model_name_or_path The path to the pre-trained config.
|
||||
* @param {PretrainedOptions} options Additional options for loading the config.
|
||||
* @throws {Error} Throws an error if the config.json is not found in the `pretrained_model_name_or_path`.
|
||||
*
|
||||
* @returns {Promise<PretrainedConfig>} A new instance of the `PretrainedConfig` class.
|
||||
*/
|
||||
static async from_pretrained(pretrained_model_name_or_path, {
|
||||
progress_callback = null,
|
||||
config = null,
|
||||
cache_dir = null,
|
||||
local_files_only = false,
|
||||
revision = 'main',
|
||||
} = {}) {
|
||||
|
||||
let data = config ?? await loadConfig(pretrained_model_name_or_path, {
|
||||
progress_callback,
|
||||
config,
|
||||
cache_dir,
|
||||
local_files_only,
|
||||
revision,
|
||||
})
|
||||
return new this(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class which is used to instantiate pretrained configs with the `from_pretrained` function.
|
||||
*
|
||||
* @example
|
||||
* let config = await AutoConfig.from_pretrained('bert-base-uncased');
|
||||
*/
|
||||
export class AutoConfig {
|
||||
/** @type {PretrainedConfig.from_pretrained} */
|
||||
static async from_pretrained(...args) {
|
||||
return PretrainedConfig.from_pretrained(...args);
|
||||
}
|
||||
}
|
||||
128
node_modules/@xenova/transformers/src/env.js
generated
vendored
Normal file
128
node_modules/@xenova/transformers/src/env.js
generated
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* @file Module used to configure Transformers.js.
|
||||
*
|
||||
* **Example:** Disable remote models.
|
||||
* ```javascript
|
||||
* import { env } from '@xenova/transformers';
|
||||
* env.allowRemoteModels = false;
|
||||
* ```
|
||||
*
|
||||
* **Example:** Set local model path.
|
||||
* ```javascript
|
||||
* import { env } from '@xenova/transformers';
|
||||
* env.localModelPath = '/path/to/local/models/';
|
||||
* ```
|
||||
*
|
||||
* **Example:** Set cache directory.
|
||||
* ```javascript
|
||||
* import { env } from '@xenova/transformers';
|
||||
* env.cacheDir = '/path/to/cache/directory/';
|
||||
* ```
|
||||
*
|
||||
* @module env
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import url from 'url';
|
||||
|
||||
import { ONNX } from './backends/onnx.js';
|
||||
const { env: onnx_env } = ONNX;
|
||||
|
||||
const VERSION = '2.17.2';
|
||||
|
||||
// Check if various APIs are available (depends on environment)
|
||||
const WEB_CACHE_AVAILABLE = typeof self !== 'undefined' && 'caches' in self;
|
||||
const FS_AVAILABLE = !isEmpty(fs); // check if file system is available
|
||||
const PATH_AVAILABLE = !isEmpty(path); // check if path is available
|
||||
|
||||
const RUNNING_LOCALLY = FS_AVAILABLE && PATH_AVAILABLE;
|
||||
|
||||
const __dirname = RUNNING_LOCALLY
|
||||
? path.dirname(path.dirname(url.fileURLToPath(import.meta.url)))
|
||||
: './';
|
||||
|
||||
// Only used for environments with access to file system
|
||||
const DEFAULT_CACHE_DIR = RUNNING_LOCALLY
|
||||
? path.join(__dirname, '/.cache/')
|
||||
: null;
|
||||
|
||||
// Set local model path, based on available APIs
|
||||
const DEFAULT_LOCAL_MODEL_PATH = '/models/';
|
||||
const localModelPath = RUNNING_LOCALLY
|
||||
? path.join(__dirname, DEFAULT_LOCAL_MODEL_PATH)
|
||||
: DEFAULT_LOCAL_MODEL_PATH;
|
||||
|
||||
if (onnx_env?.wasm) {
|
||||
// Set path to wasm files. This is needed when running in a web worker.
|
||||
// https://onnxruntime.ai/docs/api/js/interfaces/Env.WebAssemblyFlags.html#wasmPaths
|
||||
// We use remote wasm files by default to make it easier for newer users.
|
||||
// In practice, users should probably self-host the necessary .wasm files.
|
||||
onnx_env.wasm.wasmPaths = RUNNING_LOCALLY
|
||||
? path.join(__dirname, '/dist/')
|
||||
: `https://cdn.jsdelivr.net/npm/@xenova/transformers@${VERSION}/dist/`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Global variable used to control execution. This provides users a simple way to configure Transformers.js.
|
||||
* @property {Object} backends Expose environment variables of different backends,
|
||||
* allowing users to set these variables if they want to.
|
||||
* @property {string} __dirname Directory name of module. Useful for resolving local paths.
|
||||
* @property {string} version This version of Transformers.js.
|
||||
* @property {boolean} allowRemoteModels Whether to allow loading of remote files, defaults to `true`.
|
||||
* If set to `false`, it will have the same effect as setting `local_files_only=true` when loading pipelines, models, tokenizers, processors, etc.
|
||||
* @property {string} remoteHost Host URL to load models from. Defaults to the Hugging Face Hub.
|
||||
* @property {string} remotePathTemplate Path template to fill in and append to `remoteHost` when loading models.
|
||||
* @property {boolean} allowLocalModels Whether to allow loading of local files, defaults to `true`.
|
||||
* If set to `false`, it will skip the local file check and try to load the model from the remote host.
|
||||
* @property {string} localModelPath Path to load local models from. Defaults to `/models/`.
|
||||
* @property {boolean} useFS Whether to use the file system to load files. By default, it is `true` if available.
|
||||
* @property {boolean} useBrowserCache Whether to use Cache API to cache models. By default, it is `true` if available.
|
||||
* @property {boolean} useFSCache Whether to use the file system to cache files. By default, it is `true` if available.
|
||||
* @property {string} cacheDir The directory to use for caching files with the file system. By default, it is `./.cache`.
|
||||
* @property {boolean} useCustomCache Whether to use a custom cache system (defined by `customCache`), defaults to `false`.
|
||||
* @property {Object} customCache The custom cache to use. Defaults to `null`. Note: this must be an object which
|
||||
* implements the `match` and `put` functions of the Web Cache API. For more information, see https://developer.mozilla.org/en-US/docs/Web/API/Cache
|
||||
*/
|
||||
export const env = {
|
||||
/////////////////// Backends settings ///////////////////
|
||||
backends: {
|
||||
// onnxruntime-web/onnxruntime-node
|
||||
onnx: onnx_env,
|
||||
|
||||
// TensorFlow.js
|
||||
tfjs: {},
|
||||
},
|
||||
|
||||
__dirname,
|
||||
version: VERSION,
|
||||
|
||||
/////////////////// Model settings ///////////////////
|
||||
allowRemoteModels: true,
|
||||
remoteHost: 'https://huggingface.co/',
|
||||
remotePathTemplate: '{model}/resolve/{revision}/',
|
||||
|
||||
allowLocalModels: true,
|
||||
localModelPath: localModelPath,
|
||||
useFS: FS_AVAILABLE,
|
||||
|
||||
/////////////////// Cache settings ///////////////////
|
||||
useBrowserCache: WEB_CACHE_AVAILABLE,
|
||||
|
||||
useFSCache: FS_AVAILABLE,
|
||||
cacheDir: DEFAULT_CACHE_DIR,
|
||||
|
||||
useCustomCache: false,
|
||||
customCache: null,
|
||||
//////////////////////////////////////////////////////
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {Object} obj
|
||||
* @private
|
||||
*/
|
||||
function isEmpty(obj) {
|
||||
return Object.keys(obj).length === 0;
|
||||
}
|
||||
|
||||
6267
node_modules/@xenova/transformers/src/models.js
generated
vendored
Normal file
6267
node_modules/@xenova/transformers/src/models.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
3287
node_modules/@xenova/transformers/src/pipelines.js
generated
vendored
Normal file
3287
node_modules/@xenova/transformers/src/pipelines.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2248
node_modules/@xenova/transformers/src/processors.js
generated
vendored
Normal file
2248
node_modules/@xenova/transformers/src/processors.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
4479
node_modules/@xenova/transformers/src/tokenizers.js
generated
vendored
Normal file
4479
node_modules/@xenova/transformers/src/tokenizers.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
24
node_modules/@xenova/transformers/src/transformers.js
generated
vendored
Normal file
24
node_modules/@xenova/transformers/src/transformers.js
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @file Entry point for the Transformers.js library. Only the exports from this file
|
||||
* are available to the end user, and are grouped as follows:
|
||||
*
|
||||
* 1. [Pipelines](./pipelines)
|
||||
* 2. [Environment variables](./env)
|
||||
* 3. [Models](./models)
|
||||
* 4. [Tokenizers](./tokenizers)
|
||||
* 5. [Processors](./processors)
|
||||
*
|
||||
* @module transformers
|
||||
*/
|
||||
|
||||
export * from './pipelines.js';
|
||||
export * from './env.js';
|
||||
export * from './models.js';
|
||||
export * from './tokenizers.js';
|
||||
export * from './processors.js';
|
||||
export * from './configs.js';
|
||||
|
||||
export * from './utils/audio.js';
|
||||
export * from './utils/image.js';
|
||||
export * from './utils/tensor.js';
|
||||
export * from './utils/maths.js';
|
||||
672
node_modules/@xenova/transformers/src/utils/audio.js
generated
vendored
Normal file
672
node_modules/@xenova/transformers/src/utils/audio.js
generated
vendored
Normal file
@@ -0,0 +1,672 @@
|
||||
/**
|
||||
* @file Helper module for audio processing.
|
||||
*
|
||||
* These functions and classes are only used internally,
|
||||
* meaning an end-user shouldn't need to access anything here.
|
||||
*
|
||||
* @module utils/audio
|
||||
*/
|
||||
|
||||
import {
|
||||
getFile,
|
||||
} from './hub.js';
|
||||
import { FFT, max } from './maths.js';
|
||||
import {
|
||||
calculateReflectOffset,
|
||||
} from './core.js';
|
||||
|
||||
|
||||
/**
|
||||
* Helper function to read audio from a path/URL.
|
||||
* @param {string|URL} url The path/URL to load the audio from.
|
||||
* @param {number} sampling_rate The sampling rate to use when decoding the audio.
|
||||
* @returns {Promise<Float32Array>} The decoded audio as a `Float32Array`.
|
||||
*/
|
||||
export async function read_audio(url, sampling_rate) {
|
||||
if (typeof AudioContext === 'undefined') {
|
||||
// Running in node or an environment without AudioContext
|
||||
throw Error(
|
||||
"Unable to load audio from path/URL since `AudioContext` is not available in your environment. " +
|
||||
"Instead, audio data should be passed directly to the pipeline/processor. " +
|
||||
"For more information and some example code, see https://huggingface.co/docs/transformers.js/guides/node-audio-processing."
|
||||
)
|
||||
}
|
||||
|
||||
const response = await (await getFile(url)).arrayBuffer();
|
||||
const audioCTX = new AudioContext({ sampleRate: sampling_rate });
|
||||
if (typeof sampling_rate === 'undefined') {
|
||||
console.warn(`No sampling rate provided, using default of ${audioCTX.sampleRate}Hz.`)
|
||||
}
|
||||
const decoded = await audioCTX.decodeAudioData(response);
|
||||
|
||||
/** @type {Float32Array} */
|
||||
let audio;
|
||||
|
||||
// We now replicate HuggingFace's `ffmpeg_read` method:
|
||||
if (decoded.numberOfChannels === 2) {
|
||||
// When downmixing a stereo audio file to mono using the -ac 1 option in FFmpeg,
|
||||
// the audio signal is summed across both channels to create a single mono channel.
|
||||
// However, if the audio is at full scale (i.e. the highest possible volume level),
|
||||
// the summing of the two channels can cause the audio signal to clip or distort.
|
||||
|
||||
// To prevent this clipping, FFmpeg applies a scaling factor of 1/sqrt(2) (~ 0.707)
|
||||
// to the audio signal before summing the two channels. This scaling factor ensures
|
||||
// that the combined audio signal will not exceed the maximum possible level, even
|
||||
// if both channels are at full scale.
|
||||
|
||||
// After applying this scaling factor, the audio signal from both channels is summed
|
||||
// to create a single mono channel. It's worth noting that this scaling factor is
|
||||
// only applied when downmixing stereo audio to mono using the -ac 1 option in FFmpeg.
|
||||
// If you're using a different downmixing method, or if you're not downmixing the
|
||||
// audio at all, this scaling factor may not be needed.
|
||||
const SCALING_FACTOR = Math.sqrt(2);
|
||||
|
||||
const left = decoded.getChannelData(0);
|
||||
const right = decoded.getChannelData(1);
|
||||
|
||||
audio = new Float32Array(left.length);
|
||||
for (let i = 0; i < decoded.length; ++i) {
|
||||
audio[i] = SCALING_FACTOR * (left[i] + right[i]) / 2;
|
||||
}
|
||||
|
||||
} else {
|
||||
// If the audio is not stereo, we can just use the first channel:
|
||||
audio = decoded.getChannelData(0);
|
||||
}
|
||||
|
||||
return audio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a Hanning window of length M.
|
||||
*
|
||||
* @param {number} M The length of the Hanning window to generate.
|
||||
* @returns {Float64Array} The generated Hanning window.
|
||||
*/
|
||||
export function hanning(M) {
|
||||
if (M < 1) {
|
||||
return new Float64Array();
|
||||
}
|
||||
if (M === 1) {
|
||||
return new Float64Array([1]);
|
||||
}
|
||||
const denom = M - 1;
|
||||
const factor = Math.PI / denom;
|
||||
const cos_vals = new Float64Array(M);
|
||||
for (let i = 0; i < M; ++i) {
|
||||
const n = 2 * i - denom;
|
||||
cos_vals[i] = 0.5 + 0.5 * Math.cos(factor * n);
|
||||
}
|
||||
return cos_vals;
|
||||
}
|
||||
|
||||
const HERTZ_TO_MEL_MAPPING = {
|
||||
"htk": (/** @type {number} */ freq) => 2595.0 * Math.log10(1.0 + (freq / 700.0)),
|
||||
"kaldi": (/** @type {number} */ freq) => 1127.0 * Math.log(1.0 + (freq / 700.0)),
|
||||
"slaney": (/** @type {number} */ freq, min_log_hertz = 1000.0, min_log_mel = 15.0, logstep = 27.0 / Math.log(6.4)) =>
|
||||
freq >= min_log_hertz
|
||||
? min_log_mel + Math.log(freq / min_log_hertz) * logstep
|
||||
: 3.0 * freq / 200.0,
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {Float32Array|Float64Array|number} T
|
||||
* @param {T} freq
|
||||
* @param {string} [mel_scale]
|
||||
* @returns {T}
|
||||
*/
|
||||
function hertz_to_mel(freq, mel_scale = "htk") {
|
||||
const fn = HERTZ_TO_MEL_MAPPING[mel_scale];
|
||||
if (!fn) {
|
||||
throw new Error('mel_scale should be one of "htk", "slaney" or "kaldi".');
|
||||
}
|
||||
|
||||
return typeof freq === 'number' ? fn(freq) : freq.map(x => fn(x));
|
||||
}
|
||||
|
||||
const MEL_TO_HERTZ_MAPPING = {
|
||||
"htk": (/** @type {number} */ mels) => 700.0 * (10.0 ** (mels / 2595.0) - 1.0),
|
||||
"kaldi": (/** @type {number} */ mels) => 700.0 * (Math.exp(mels / 1127.0) - 1.0),
|
||||
"slaney": (/** @type {number} */ mels, min_log_hertz = 1000.0, min_log_mel = 15.0, logstep = Math.log(6.4) / 27.0) => mels >= min_log_mel
|
||||
? min_log_hertz * Math.exp(logstep * (mels - min_log_mel))
|
||||
: 200.0 * mels / 3.0,
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {Float32Array|Float64Array|number} T
|
||||
* @param {T} mels
|
||||
* @param {string} [mel_scale]
|
||||
* @returns {T}
|
||||
*/
|
||||
function mel_to_hertz(mels, mel_scale = "htk") {
|
||||
const fn = MEL_TO_HERTZ_MAPPING[mel_scale];
|
||||
if (!fn) {
|
||||
throw new Error('mel_scale should be one of "htk", "slaney" or "kaldi".');
|
||||
}
|
||||
|
||||
return typeof mels === 'number' ? fn(mels) : mels.map(x => fn(x));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a triangular filter bank.
|
||||
*
|
||||
* Adapted from torchaudio and librosa.
|
||||
*
|
||||
* @param {Float64Array} fft_freqs Discrete frequencies of the FFT bins in Hz, of shape `(num_frequency_bins,)`.
|
||||
* @param {Float64Array} filter_freqs Center frequencies of the triangular filters to create, in Hz, of shape `(num_mel_filters,)`.
|
||||
* @returns {number[][]} of shape `(num_frequency_bins, num_mel_filters)`.
|
||||
*/
|
||||
function _create_triangular_filter_bank(fft_freqs, filter_freqs) {
|
||||
const filter_diff = Float64Array.from(
|
||||
{ length: filter_freqs.length - 1 },
|
||||
(_, i) => filter_freqs[i + 1] - filter_freqs[i]
|
||||
);
|
||||
|
||||
const slopes = Array.from({
|
||||
length: fft_freqs.length
|
||||
}, () => new Array(filter_freqs.length));
|
||||
|
||||
for (let j = 0; j < fft_freqs.length; ++j) {
|
||||
const slope = slopes[j];
|
||||
for (let i = 0; i < filter_freqs.length; ++i) {
|
||||
slope[i] = filter_freqs[i] - fft_freqs[j];
|
||||
}
|
||||
}
|
||||
|
||||
const numFreqs = filter_freqs.length - 2;
|
||||
const ret = Array.from({ length: numFreqs }, () => new Array(fft_freqs.length));
|
||||
|
||||
for (let j = 0; j < fft_freqs.length; ++j) { // 201
|
||||
const slope = slopes[j];
|
||||
for (let i = 0; i < numFreqs; ++i) { // 80
|
||||
const down = -slope[i] / filter_diff[i];
|
||||
const up = slope[i + 2] / filter_diff[i + 1];
|
||||
ret[i][j] = Math.max(0, Math.min(down, up));
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return evenly spaced numbers over a specified interval.
|
||||
* @param {number} start The starting value of the sequence.
|
||||
* @param {number} end The end value of the sequence.
|
||||
* @param {number} num Number of samples to generate.
|
||||
* @returns `num` evenly spaced samples, calculated over the interval `[start, stop]`.
|
||||
*/
|
||||
function linspace(start, end, num) {
|
||||
const step = (end - start) / (num - 1);
|
||||
return Float64Array.from({ length: num }, (_, i) => start + step * i);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a frequency bin conversion matrix used to obtain a mel spectrogram. This is called a *mel filter bank*, and
|
||||
* various implementation exist, which differ in the number of filters, the shape of the filters, the way the filters
|
||||
* are spaced, the bandwidth of the filters, and the manner in which the spectrum is warped. The goal of these
|
||||
* features is to approximate the non-linear human perception of the variation in pitch with respect to the frequency.
|
||||
* @param {number} num_frequency_bins Number of frequencies used to compute the spectrogram (should be the same as in `stft`).
|
||||
* @param {number} num_mel_filters Number of mel filters to generate.
|
||||
* @param {number} min_frequency Lowest frequency of interest in Hz.
|
||||
* @param {number} max_frequency Highest frequency of interest in Hz. This should not exceed `sampling_rate / 2`.
|
||||
* @param {number} sampling_rate Sample rate of the audio waveform.
|
||||
* @param {string} [norm] If `"slaney"`, divide the triangular mel weights by the width of the mel band (area normalization).
|
||||
* @param {string} [mel_scale] The mel frequency scale to use, `"htk"` or `"slaney"`.
|
||||
* @param {boolean} [triangularize_in_mel_space] If this option is enabled, the triangular filter is applied in mel space rather than frequency space.
|
||||
* This should be set to `true` in order to get the same results as `torchaudio` when computing mel filters.
|
||||
* @returns {number[][]} Triangular filter bank matrix, which is a 2D array of shape (`num_frequency_bins`, `num_mel_filters`).
|
||||
* This is a projection matrix to go from a spectrogram to a mel spectrogram.
|
||||
*/
|
||||
export function mel_filter_bank(
|
||||
num_frequency_bins,
|
||||
num_mel_filters,
|
||||
min_frequency,
|
||||
max_frequency,
|
||||
sampling_rate,
|
||||
norm = null,
|
||||
mel_scale = "htk",
|
||||
triangularize_in_mel_space = false,
|
||||
) {
|
||||
if (norm !== null && norm !== "slaney") {
|
||||
throw new Error('norm must be one of null or "slaney"');
|
||||
}
|
||||
|
||||
const mel_min = hertz_to_mel(min_frequency, mel_scale);
|
||||
const mel_max = hertz_to_mel(max_frequency, mel_scale);
|
||||
const mel_freqs = linspace(mel_min, mel_max, num_mel_filters + 2);
|
||||
|
||||
let filter_freqs = mel_to_hertz(mel_freqs, mel_scale);
|
||||
let fft_freqs; // frequencies of FFT bins in Hz
|
||||
|
||||
if (triangularize_in_mel_space) {
|
||||
const fft_bin_width = sampling_rate / (num_frequency_bins * 2);
|
||||
fft_freqs = hertz_to_mel(Float64Array.from({ length: num_frequency_bins }, (_, i) => i * fft_bin_width), mel_scale);
|
||||
filter_freqs = mel_freqs;
|
||||
} else {
|
||||
fft_freqs = linspace(0, Math.floor(sampling_rate / 2), num_frequency_bins);
|
||||
}
|
||||
|
||||
const mel_filters = _create_triangular_filter_bank(fft_freqs, filter_freqs);
|
||||
|
||||
if (norm !== null && norm === "slaney") {
|
||||
// Slaney-style mel is scaled to be approx constant energy per channel
|
||||
for (let i = 0; i < num_mel_filters; ++i) {
|
||||
const filter = mel_filters[i];
|
||||
const enorm = 2.0 / (filter_freqs[i + 2] - filter_freqs[i]);
|
||||
for (let j = 0; j < num_frequency_bins; ++j) {
|
||||
// Apply this enorm to all frequency bins
|
||||
filter[j] *= enorm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO warn if there is a zero row
|
||||
|
||||
return mel_filters;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {Float32Array|Float64Array} T
|
||||
* Pads an array with a reflected version of itself on both ends.
|
||||
* @param {T} array The array to pad.
|
||||
* @param {number} left The amount of padding to add to the left.
|
||||
* @param {number} right The amount of padding to add to the right.
|
||||
* @returns {T} The padded array.
|
||||
*/
|
||||
function padReflect(array, left, right) {
|
||||
// @ts-ignore
|
||||
const padded = new array.constructor(array.length + left + right);
|
||||
const w = array.length - 1;
|
||||
|
||||
for (let i = 0; i < array.length; ++i) {
|
||||
padded[left + i] = array[i];
|
||||
}
|
||||
|
||||
for (let i = 1; i <= left; ++i) {
|
||||
padded[left - i] = array[calculateReflectOffset(i, w)];
|
||||
}
|
||||
|
||||
for (let i = 1; i <= right; ++i) {
|
||||
padded[w + left + i] = array[calculateReflectOffset(w - i, w)];
|
||||
}
|
||||
|
||||
return padded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to compute `amplitude_to_db` and `power_to_db`.
|
||||
* @template {Float32Array|Float64Array} T
|
||||
* @param {T} spectrogram
|
||||
* @param {number} factor
|
||||
* @param {number} reference
|
||||
* @param {number} min_value
|
||||
* @param {number} db_range
|
||||
* @returns {T}
|
||||
*/
|
||||
function _db_conversion_helper(spectrogram, factor, reference, min_value, db_range) {
|
||||
if (reference <= 0) {
|
||||
throw new Error('reference must be greater than zero');
|
||||
}
|
||||
|
||||
if (min_value <= 0) {
|
||||
throw new Error('min_value must be greater than zero');
|
||||
}
|
||||
|
||||
reference = Math.max(min_value, reference);
|
||||
|
||||
const logReference = Math.log10(reference);
|
||||
for (let i = 0; i < spectrogram.length; ++i) {
|
||||
spectrogram[i] = factor * Math.log10(Math.max(min_value, spectrogram[i]) - logReference)
|
||||
}
|
||||
|
||||
if (db_range !== null) {
|
||||
if (db_range <= 0) {
|
||||
throw new Error('db_range must be greater than zero');
|
||||
}
|
||||
const maxValue = max(spectrogram)[0] - db_range;
|
||||
for (let i = 0; i < spectrogram.length; ++i) {
|
||||
spectrogram[i] = Math.max(spectrogram[i], maxValue);
|
||||
}
|
||||
}
|
||||
|
||||
return spectrogram;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an amplitude spectrogram to the decibel scale. This computes `20 * log10(spectrogram / reference)`,
|
||||
* using basic logarithm properties for numerical stability. NOTE: Operates in-place.
|
||||
*
|
||||
* The motivation behind applying the log function on the (mel) spectrogram is that humans do not hear loudness on a
|
||||
* linear scale. Generally to double the perceived volume of a sound we need to put 8 times as much energy into it.
|
||||
* This means that large variations in energy may not sound all that different if the sound is loud to begin with.
|
||||
* This compression operation makes the (mel) spectrogram features match more closely what humans actually hear.
|
||||
*
|
||||
* @template {Float32Array|Float64Array} T
|
||||
* @param {T} spectrogram The input amplitude (mel) spectrogram.
|
||||
* @param {number} [reference=1.0] Sets the input spectrogram value that corresponds to 0 dB.
|
||||
* For example, use `np.max(spectrogram)` to set the loudest part to 0 dB. Must be greater than zero.
|
||||
* @param {number} [min_value=1e-5] The spectrogram will be clipped to this minimum value before conversion to decibels,
|
||||
* to avoid taking `log(0)`. The default of `1e-5` corresponds to a minimum of -100 dB. Must be greater than zero.
|
||||
* @param {number} [db_range=null] Sets the maximum dynamic range in decibels. For example, if `db_range = 80`, the
|
||||
* difference between the peak value and the smallest value will never be more than 80 dB. Must be greater than zero.
|
||||
* @returns {T} The modified spectrogram in decibels.
|
||||
*/
|
||||
function amplitude_to_db(spectrogram, reference = 1.0, min_value = 1e-5, db_range = null) {
|
||||
return _db_conversion_helper(spectrogram, 20.0, reference, min_value, db_range);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a power spectrogram to the decibel scale. This computes `10 * log10(spectrogram / reference)`,
|
||||
* using basic logarithm properties for numerical stability. NOTE: Operates in-place.
|
||||
*
|
||||
* The motivation behind applying the log function on the (mel) spectrogram is that humans do not hear loudness on a
|
||||
* linear scale. Generally to double the perceived volume of a sound we need to put 8 times as much energy into it.
|
||||
* This means that large variations in energy may not sound all that different if the sound is loud to begin with.
|
||||
* This compression operation makes the (mel) spectrogram features match more closely what humans actually hear.
|
||||
*
|
||||
* Based on the implementation of `librosa.power_to_db`.
|
||||
*
|
||||
* @template {Float32Array|Float64Array} T
|
||||
* @param {T} spectrogram The input power (mel) spectrogram. Note that a power spectrogram has the amplitudes squared!
|
||||
* @param {number} [reference=1.0] Sets the input spectrogram value that corresponds to 0 dB.
|
||||
* For example, use `np.max(spectrogram)` to set the loudest part to 0 dB. Must be greater than zero.
|
||||
* @param {number} [min_value=1e-10] The spectrogram will be clipped to this minimum value before conversion to decibels,
|
||||
* to avoid taking `log(0)`. The default of `1e-10` corresponds to a minimum of -100 dB. Must be greater than zero.
|
||||
* @param {number} [db_range=null] Sets the maximum dynamic range in decibels. For example, if `db_range = 80`, the
|
||||
* difference between the peak value and the smallest value will never be more than 80 dB. Must be greater than zero.
|
||||
* @returns {T} The modified spectrogram in decibels.
|
||||
*/
|
||||
function power_to_db(spectrogram, reference = 1.0, min_value = 1e-10, db_range = null) {
|
||||
return _db_conversion_helper(spectrogram, 10.0, reference, min_value, db_range);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates a spectrogram over one waveform using the Short-Time Fourier Transform.
|
||||
*
|
||||
* This function can create the following kinds of spectrograms:
|
||||
* - amplitude spectrogram (`power = 1.0`)
|
||||
* - power spectrogram (`power = 2.0`)
|
||||
* - complex-valued spectrogram (`power = None`)
|
||||
* - log spectrogram (use `log_mel` argument)
|
||||
* - mel spectrogram (provide `mel_filters`)
|
||||
* - log-mel spectrogram (provide `mel_filters` and `log_mel`)
|
||||
*
|
||||
* In this implementation, the window is assumed to be zero-padded to have the same size as the analysis frame.
|
||||
* A padded window can be obtained from `window_function()`. The FFT input buffer may be larger than the analysis frame,
|
||||
* typically the next power of two.
|
||||
*
|
||||
* @param {Float32Array|Float64Array} waveform The input waveform of shape `(length,)`. This must be a single real-valued, mono waveform.
|
||||
* @param {Float32Array|Float64Array} window The windowing function to apply of shape `(frame_length,)`, including zero-padding if necessary. The actual window length may be
|
||||
* shorter than `frame_length`, but we're assuming the array has already been zero-padded.
|
||||
* @param {number} frame_length The length of the analysis frames in samples (a.k.a., `fft_length`).
|
||||
* @param {number} hop_length The stride between successive analysis frames in samples.
|
||||
* @param {Object} options
|
||||
* @param {number} [options.fft_length=null] The size of the FFT buffer in samples. This determines how many frequency bins the spectrogram will have.
|
||||
* For optimal speed, this should be a power of two. If `null`, uses `frame_length`.
|
||||
* @param {number} [options.power=1.0] If 1.0, returns the amplitude spectrogram. If 2.0, returns the power spectrogram. If `null`, returns complex numbers.
|
||||
* @param {boolean} [options.center=true] Whether to pad the waveform so that frame `t` is centered around time `t * hop_length`. If `false`, frame
|
||||
* `t` will start at time `t * hop_length`.
|
||||
* @param {string} [options.pad_mode="reflect"] Padding mode used when `center` is `true`. Possible values are: `"constant"` (pad with zeros),
|
||||
* `"edge"` (pad with edge values), `"reflect"` (pads with mirrored values).
|
||||
* @param {boolean} [options.onesided=true] If `true`, only computes the positive frequencies and returns a spectrogram containing `fft_length // 2 + 1`
|
||||
* frequency bins. If `false`, also computes the negative frequencies and returns `fft_length` frequency bins.
|
||||
* @param {number} [options.preemphasis=null] Coefficient for a low-pass filter that applies pre-emphasis before the DFT.
|
||||
* @param {number[][]} [options.mel_filters=null] The mel filter bank of shape `(num_freq_bins, num_mel_filters)`.
|
||||
* If supplied, applies this filter bank to create a mel spectrogram.
|
||||
* @param {number} [options.mel_floor=1e-10] Minimum value of mel frequency banks.
|
||||
* @param {string} [options.log_mel=null] How to convert the spectrogram to log scale. Possible options are:
|
||||
* `null` (don't convert), `"log"` (take the natural logarithm) `"log10"` (take the base-10 logarithm), `"dB"` (convert to decibels).
|
||||
* Can only be used when `power` is not `null`.
|
||||
* @param {number} [options.reference=1.0] Sets the input spectrogram value that corresponds to 0 dB. For example, use `max(spectrogram)[0]` to set
|
||||
* the loudest part to 0 dB. Must be greater than zero.
|
||||
* @param {number} [options.min_value=1e-10] The spectrogram will be clipped to this minimum value before conversion to decibels, to avoid taking `log(0)`.
|
||||
* For a power spectrogram, the default of `1e-10` corresponds to a minimum of -100 dB. For an amplitude spectrogram, the value `1e-5` corresponds to -100 dB.
|
||||
* Must be greater than zero.
|
||||
* @param {number} [options.db_range=null] Sets the maximum dynamic range in decibels. For example, if `db_range = 80`, the difference between the
|
||||
* peak value and the smallest value will never be more than 80 dB. Must be greater than zero.
|
||||
* @param {boolean} [options.remove_dc_offset=null] Subtract mean from waveform on each frame, applied before pre-emphasis. This should be set to `true` in
|
||||
* order to get the same results as `torchaudio.compliance.kaldi.fbank` when computing mel filters.
|
||||
* @param {number} [options.max_num_frames=null] If provided, limits the number of frames to compute to this value.
|
||||
* @param {boolean} [options.do_pad=true] If `true`, pads the output spectrogram to have `max_num_frames` frames.
|
||||
* @param {boolean} [options.transpose=false] If `true`, the returned spectrogram will have shape `(num_frames, num_frequency_bins/num_mel_filters)`. If `false`, the returned spectrogram will have shape `(num_frequency_bins/num_mel_filters, num_frames)`.
|
||||
* @returns {{data: Float32Array, dims: number[]}} Spectrogram of shape `(num_frequency_bins, length)` (regular spectrogram) or shape `(num_mel_filters, length)` (mel spectrogram).
|
||||
*/
|
||||
export function spectrogram(
|
||||
waveform,
|
||||
window,
|
||||
frame_length,
|
||||
hop_length,
|
||||
{
|
||||
fft_length = null,
|
||||
power = 1.0,
|
||||
center = true,
|
||||
pad_mode = "reflect",
|
||||
onesided = true,
|
||||
preemphasis = null,
|
||||
mel_filters = null,
|
||||
mel_floor = 1e-10,
|
||||
log_mel = null,
|
||||
reference = 1.0,
|
||||
min_value = 1e-10,
|
||||
db_range = null,
|
||||
remove_dc_offset = null,
|
||||
|
||||
// Custom parameters for efficiency reasons
|
||||
max_num_frames = null,
|
||||
do_pad = true,
|
||||
transpose = false,
|
||||
} = {}
|
||||
) {
|
||||
const window_length = window.length;
|
||||
if (fft_length === null) {
|
||||
fft_length = frame_length;
|
||||
}
|
||||
if (frame_length > fft_length) {
|
||||
throw Error(`frame_length (${frame_length}) may not be larger than fft_length (${fft_length})`)
|
||||
}
|
||||
|
||||
if (window_length !== frame_length) {
|
||||
throw new Error(`Length of the window (${window_length}) must equal frame_length (${frame_length})`);
|
||||
}
|
||||
|
||||
if (hop_length <= 0) {
|
||||
throw new Error("hop_length must be greater than zero");
|
||||
}
|
||||
|
||||
if (power === null && mel_filters !== null) {
|
||||
throw new Error(
|
||||
"You have provided `mel_filters` but `power` is `None`. Mel spectrogram computation is not yet supported for complex-valued spectrogram. " +
|
||||
"Specify `power` to fix this issue."
|
||||
);
|
||||
}
|
||||
|
||||
if (center) {
|
||||
if (pad_mode !== 'reflect') {
|
||||
throw new Error(`pad_mode="${pad_mode}" not implemented yet.`)
|
||||
}
|
||||
const half_window = Math.floor((fft_length - 1) / 2) + 1;
|
||||
waveform = padReflect(waveform, half_window, half_window);
|
||||
}
|
||||
|
||||
// split waveform into frames of frame_length size
|
||||
const num_frames = Math.floor(1 + Math.floor((waveform.length - frame_length) / hop_length))
|
||||
|
||||
const num_frequency_bins = onesided ? Math.floor(fft_length / 2) + 1 : fft_length
|
||||
|
||||
let d1 = num_frames;
|
||||
let d1Max = num_frames;
|
||||
|
||||
// If maximum number of frames is provided, we must either pad or truncate
|
||||
if (max_num_frames !== null) {
|
||||
if (max_num_frames > num_frames) { // input is too short, so we pad
|
||||
if (do_pad) {
|
||||
d1Max = max_num_frames;
|
||||
}
|
||||
} else { // input is too long, so we truncate
|
||||
d1Max = d1 = max_num_frames;
|
||||
}
|
||||
}
|
||||
|
||||
// Preallocate arrays to store output.
|
||||
const fft = new FFT(fft_length);
|
||||
const inputBuffer = new Float64Array(fft_length);
|
||||
const outputBuffer = new Float64Array(fft.outputBufferSize);
|
||||
const magnitudes = new Array(d1);
|
||||
|
||||
for (let i = 0; i < d1; ++i) {
|
||||
// Populate buffer with waveform data
|
||||
const offset = i * hop_length;
|
||||
for (let j = 0; j < frame_length; ++j) {
|
||||
inputBuffer[j] = waveform[offset + j];
|
||||
}
|
||||
|
||||
if (remove_dc_offset) {
|
||||
let sum = 0;
|
||||
for (let j = 0; j < frame_length; ++j) {
|
||||
sum += inputBuffer[j];
|
||||
}
|
||||
const mean = sum / frame_length;
|
||||
for (let j = 0; j < frame_length; ++j) {
|
||||
inputBuffer[j] -= mean;
|
||||
}
|
||||
}
|
||||
|
||||
if (preemphasis !== null) {
|
||||
// Done in reverse to avoid copies and distructive modification
|
||||
for (let j = frame_length - 1; j >= 1; --j) {
|
||||
inputBuffer[j] -= preemphasis * inputBuffer[j - 1];
|
||||
}
|
||||
inputBuffer[0] *= 1 - preemphasis;
|
||||
}
|
||||
|
||||
for (let j = 0; j < window.length; ++j) {
|
||||
inputBuffer[j] *= window[j];
|
||||
}
|
||||
|
||||
fft.realTransform(outputBuffer, inputBuffer);
|
||||
|
||||
// compute magnitudes
|
||||
const row = new Array(num_frequency_bins);
|
||||
for (let j = 0; j < row.length; ++j) {
|
||||
const j2 = j << 1;
|
||||
row[j] = outputBuffer[j2] ** 2 + outputBuffer[j2 + 1] ** 2;
|
||||
}
|
||||
magnitudes[i] = row;
|
||||
}
|
||||
|
||||
if (power !== null && power !== 2) {
|
||||
// slight optimization to not sqrt
|
||||
const pow = 2 / power; // we use 2 since we already squared
|
||||
for (let i = 0; i < magnitudes.length; ++i) {
|
||||
const magnitude = magnitudes[i];
|
||||
for (let j = 0; j < magnitude.length; ++j) {
|
||||
magnitude[j] **= pow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: What if `mel_filters` is null?
|
||||
const num_mel_filters = mel_filters.length;
|
||||
|
||||
// Only here do we create Float32Array
|
||||
const mel_spec = new Float32Array(num_mel_filters * d1Max);
|
||||
|
||||
// Perform matrix muliplication:
|
||||
// mel_spec = mel_filters @ magnitudes.T
|
||||
// - mel_filters.shape=(80, 201)
|
||||
// - magnitudes.shape=(3000, 201) => - magnitudes.T.shape=(201, 3000)
|
||||
// - mel_spec.shape=(80, 3000)
|
||||
const dims = transpose ? [d1Max, num_mel_filters] : [num_mel_filters, d1Max];
|
||||
for (let i = 0; i < num_mel_filters; ++i) { // num melfilters (e.g., 80)
|
||||
const filter = mel_filters[i];
|
||||
for (let j = 0; j < d1; ++j) { // num frames (e.g., 3000)
|
||||
const magnitude = magnitudes[j];
|
||||
|
||||
let sum = 0;
|
||||
for (let k = 0; k < num_frequency_bins; ++k) { // num frequency bins (e.g., 201)
|
||||
sum += filter[k] * magnitude[k];
|
||||
}
|
||||
|
||||
mel_spec[
|
||||
transpose
|
||||
? j * num_mel_filters + i
|
||||
: i * d1 + j
|
||||
] = Math.max(mel_floor, sum);
|
||||
}
|
||||
}
|
||||
|
||||
if (power !== null && log_mel !== null) {
|
||||
const o = Math.min(mel_spec.length, d1 * num_mel_filters);
|
||||
switch (log_mel) {
|
||||
case 'log':
|
||||
for (let i = 0; i < o; ++i) {
|
||||
mel_spec[i] = Math.log(mel_spec[i]);
|
||||
}
|
||||
break;
|
||||
case 'log10':
|
||||
for (let i = 0; i < o; ++i) {
|
||||
mel_spec[i] = Math.log10(mel_spec[i]);
|
||||
}
|
||||
break;
|
||||
case 'dB':
|
||||
if (power === 1.0) {
|
||||
// NOTE: operates in-place
|
||||
amplitude_to_db(mel_spec, reference, min_value, db_range);
|
||||
} else if (power === 2.0) {
|
||||
power_to_db(mel_spec, reference, min_value, db_range);
|
||||
} else {
|
||||
throw new Error(`Cannot use log_mel option '${log_mel}' with power ${power}`)
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(`log_mel must be one of null, 'log', 'log10' or 'dB'. Got '${log_mel}'`);
|
||||
}
|
||||
}
|
||||
|
||||
return { data: mel_spec, dims };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array containing the specified window.
|
||||
* @param {number} window_length The length of the window in samples.
|
||||
* @param {string} name The name of the window function.
|
||||
* @param {Object} options Additional options.
|
||||
* @param {boolean} [options.periodic=true] Whether the window is periodic or symmetric.
|
||||
* @param {number} [options.frame_length=null] The length of the analysis frames in samples.
|
||||
* Provide a value for `frame_length` if the window is smaller than the frame length, so that it will be zero-padded.
|
||||
* @param {boolean} [options.center=true] Whether to center the window inside the FFT buffer. Only used when `frame_length` is provided.
|
||||
* @returns {Float64Array} The window of shape `(window_length,)` or `(frame_length,)`.
|
||||
*/
|
||||
export function window_function(window_length, name, {
|
||||
periodic = true,
|
||||
frame_length = null,
|
||||
center = true,
|
||||
} = {}) {
|
||||
const length = periodic ? window_length + 1 : window_length;
|
||||
let window;
|
||||
switch (name) {
|
||||
case 'boxcar':
|
||||
window = new Float64Array(length).fill(1.0);
|
||||
break;
|
||||
case 'hann':
|
||||
case 'hann_window':
|
||||
window = hanning(length);
|
||||
break;
|
||||
case 'povey':
|
||||
window = hanning(length).map(x => Math.pow(x, 0.85));
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown window type ${name}.`);
|
||||
}
|
||||
if (periodic) {
|
||||
window = window.subarray(0, window_length);
|
||||
}
|
||||
if (frame_length === null) {
|
||||
return window;
|
||||
}
|
||||
if (window_length > frame_length) {
|
||||
throw new Error(`Length of the window (${window_length}) may not be larger than frame_length (${frame_length})`);
|
||||
}
|
||||
|
||||
return window;
|
||||
}
|
||||
175
node_modules/@xenova/transformers/src/utils/core.js
generated
vendored
Normal file
175
node_modules/@xenova/transformers/src/utils/core.js
generated
vendored
Normal file
@@ -0,0 +1,175 @@
|
||||
|
||||
/**
|
||||
* @file Core utility functions/classes for Transformers.js.
|
||||
*
|
||||
* These are only used internally, meaning an end-user shouldn't
|
||||
* need to access anything here.
|
||||
*
|
||||
* @module utils/core
|
||||
*/
|
||||
|
||||
/**
|
||||
* Helper function to dispatch progress callbacks.
|
||||
*
|
||||
* @param {Function} progress_callback The progress callback function to dispatch.
|
||||
* @param {any} data The data to pass to the progress callback function.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
export function dispatchCallback(progress_callback, data) {
|
||||
if (progress_callback) progress_callback(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverses the keys and values of an object.
|
||||
*
|
||||
* @param {Object} data The object to reverse.
|
||||
* @returns {Object} The reversed object.
|
||||
* @see https://ultimatecourses.com/blog/reverse-object-keys-and-values-in-javascript
|
||||
*/
|
||||
export function reverseDictionary(data) {
|
||||
// https://ultimatecourses.com/blog/reverse-object-keys-and-values-in-javascript
|
||||
return Object.fromEntries(Object.entries(data).map(([key, value]) => [value, key]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes regular expression special characters from a string by replacing them with their escaped counterparts.
|
||||
*
|
||||
* @param {string} string The string to escape.
|
||||
* @returns {string} The escaped string.
|
||||
*/
|
||||
export function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
|
||||
/**
|
||||
* A base class for creating callable objects.
|
||||
*
|
||||
* @type {new () => {(...args: any[]): any, _call(...args: any[]): any}}
|
||||
*/
|
||||
export const Callable = /** @type {any} */ (class {
|
||||
/**
|
||||
* Creates a new instance of the Callable class.
|
||||
*/
|
||||
constructor() {
|
||||
/**
|
||||
* Creates a closure that delegates to a private method '_call' with the given arguments.
|
||||
* @type {any}
|
||||
* @param {...any} args Zero or more arguments to pass to the '_call' method.
|
||||
* @returns {*} The result of calling the '_call' method.
|
||||
*/
|
||||
let closure = function (...args) {
|
||||
return closure._call(...args)
|
||||
}
|
||||
return Object.setPrototypeOf(closure, new.target.prototype)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method should be implemented in subclasses to provide the
|
||||
* functionality of the callable object.
|
||||
*
|
||||
* @param {any[]} args
|
||||
* @throws {Error} If the subclass does not implement the `_call` method.
|
||||
*/
|
||||
_call(...args) {
|
||||
throw Error('Must implement _call method in subclass')
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Check if a value is a typed array.
|
||||
* @param {*} val The value to check.
|
||||
* @returns {boolean} True if the value is a `TypedArray`, false otherwise.
|
||||
*
|
||||
* Adapted from https://stackoverflow.com/a/71091338/13989043
|
||||
*/
|
||||
export function isTypedArray(val) {
|
||||
return val?.prototype?.__proto__?.constructor?.name === 'TypedArray';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if a value is an integer.
|
||||
* @param {*} x The value to check.
|
||||
* @returns {boolean} True if the value is a string, false otherwise.
|
||||
*/
|
||||
export function isIntegralNumber(x) {
|
||||
return Number.isInteger(x) || typeof x === 'bigint'
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is exists.
|
||||
* @param {*} x The value to check.
|
||||
* @returns {boolean} True if the value exists, false otherwise.
|
||||
*/
|
||||
export function exists(x) {
|
||||
return x !== undefined && x !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the dimensions of a nested array.
|
||||
*
|
||||
* @param {any[]} arr The nested array to calculate dimensions for.
|
||||
* @returns {number[]} An array containing the dimensions of the input array.
|
||||
*/
|
||||
export function calculateDimensions(arr) {
|
||||
const dimensions = [];
|
||||
let current = arr;
|
||||
while (Array.isArray(current)) {
|
||||
dimensions.push(current.length);
|
||||
current = current[0];
|
||||
}
|
||||
return dimensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replicate python's .pop() method for objects.
|
||||
* @param {Object} obj The object to pop from.
|
||||
* @param {string} key The key to pop.
|
||||
* @param {*} defaultValue The default value to return if the key does not exist.
|
||||
* @returns {*} The value of the popped key.
|
||||
* @throws {Error} If the key does not exist and no default value is provided.
|
||||
*/
|
||||
export function pop(obj, key, defaultValue = undefined) {
|
||||
const value = obj[key];
|
||||
if (value !== undefined) {
|
||||
delete obj[key];
|
||||
return value;
|
||||
}
|
||||
if (defaultValue === undefined) {
|
||||
throw Error(`Key ${key} does not exist in object.`)
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Efficiently merge arrays, creating a new copy.
|
||||
* Adapted from https://stackoverflow.com/a/6768642/13989043
|
||||
* @param {Array[]} arrs Arrays to merge.
|
||||
* @returns {Array} The merged array.
|
||||
*/
|
||||
export function mergeArrays(...arrs) {
|
||||
return Array.prototype.concat.apply([], arrs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the Cartesian product of given arrays
|
||||
* @param {...Array} a Arrays to compute the product
|
||||
* @returns {Array} Returns the computed Cartesian product as an array
|
||||
* @private
|
||||
*/
|
||||
export function product(...a) {
|
||||
// Cartesian product of items
|
||||
// Adapted from https://stackoverflow.com/a/43053803
|
||||
return a.reduce((a, b) => a.flatMap(d => b.map(e => [d, e])));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the index offset for a given index and window size.
|
||||
* @param {number} i The index.
|
||||
* @param {number} w The window size.
|
||||
* @returns {number} The index offset.
|
||||
*/
|
||||
export function calculateReflectOffset(i, w) {
|
||||
return Math.abs((i + w) % (2 * w) - w);
|
||||
}
|
||||
415
node_modules/@xenova/transformers/src/utils/data-structures.js
generated
vendored
Normal file
415
node_modules/@xenova/transformers/src/utils/data-structures.js
generated
vendored
Normal file
@@ -0,0 +1,415 @@
|
||||
|
||||
/**
|
||||
* @file Custom data structures.
|
||||
*
|
||||
* These are only used internally, meaning an end-user shouldn't
|
||||
* need to access anything here.
|
||||
*
|
||||
* @module utils/data-structures
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Efficient Heap-based Implementation of a Priority Queue.
|
||||
* It uses an array-based binary heap, where the root is at index `0`, and the
|
||||
* children of node `i` are located at indices `2i + 1` and `2i + 2`, respectively.
|
||||
*
|
||||
* Adapted from the following sources:
|
||||
* - https://stackoverflow.com/a/42919752/13989043 (original)
|
||||
* - https://github.com/belladoreai/llama-tokenizer-js (minor improvements)
|
||||
*/
|
||||
export class PriorityQueue {
|
||||
|
||||
/**
|
||||
* Create a new PriorityQueue.
|
||||
* @param {Function} comparator Comparator function to determine priority. Defaults to a MaxHeap.
|
||||
*/
|
||||
constructor(comparator = (a, b) => a > b) {
|
||||
this._heap = [];
|
||||
this._comparator = comparator;
|
||||
}
|
||||
|
||||
/**
|
||||
* The size of the queue
|
||||
*/
|
||||
get size() {
|
||||
return this._heap.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the queue is empty.
|
||||
* @returns {boolean} `true` if the queue is empty, `false` otherwise.
|
||||
*/
|
||||
isEmpty() {
|
||||
return this.size === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the element with the highest priority in the queue.
|
||||
* @returns {any} The highest priority element in the queue.
|
||||
*/
|
||||
peek() {
|
||||
return this._heap[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add one or more elements to the queue.
|
||||
* @param {...any} values The values to push into the queue.
|
||||
* @returns {number} The new size of the queue.
|
||||
*/
|
||||
push(...values) {
|
||||
return this.extend(values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple elements to the queue.
|
||||
* @param {any[]} values The values to push into the queue.
|
||||
* @returns {number} The new size of the queue.
|
||||
*/
|
||||
extend(values) {
|
||||
for (const value of values) {
|
||||
this._heap.push(value);
|
||||
this._siftUp();
|
||||
}
|
||||
return this.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove and return the element with the highest priority in the queue.
|
||||
* @returns {any} The element with the highest priority in the queue.
|
||||
*/
|
||||
pop() {
|
||||
const poppedValue = this.peek();
|
||||
const bottom = this.size - 1;
|
||||
if (bottom > 0) {
|
||||
this._swap(0, bottom);
|
||||
}
|
||||
this._heap.pop();
|
||||
this._siftDown();
|
||||
return poppedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the element with the highest priority in the queue with a new value.
|
||||
* @param {*} value The new value.
|
||||
* @returns {*} The replaced value.
|
||||
*/
|
||||
replace(value) {
|
||||
const replacedValue = this.peek();
|
||||
this._heap[0] = value;
|
||||
this._siftDown();
|
||||
return replacedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the index for the parent of the node at index `i`.
|
||||
* @param {number} i The index of the node to get the parent of.
|
||||
* @returns {number} The index of the parent node.
|
||||
* @private
|
||||
*/
|
||||
_parent(i) {
|
||||
return ((i + 1) >>> 1) - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the index for the left child of the node at index `i`.
|
||||
* @param {number} i The index of the node to get the left child of.
|
||||
* @returns {number} The index of the left child.
|
||||
* @private
|
||||
*/
|
||||
_left(i) {
|
||||
return (i << 1) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the index for the right child of the node at index `i`.
|
||||
* @param {number} i The index of the node to get the right child of.
|
||||
* @returns {number} The index of the right child.
|
||||
* @private
|
||||
*/
|
||||
_right(i) {
|
||||
return (i + 1) << 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the element at index `i` is greater than the element at index `j`.
|
||||
* @param {number} i The index of the first element to compare.
|
||||
* @param {number} j The index of the second element to compare.
|
||||
* @returns {boolean} `true` if the element at index `i` is greater than the element at index `j`, `false` otherwise.
|
||||
* @private
|
||||
*/
|
||||
_greater(i, j) {
|
||||
return this._comparator(this._heap[i], this._heap[j]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap the elements at indices `i` and `j`.
|
||||
* @param {number} i The index of the first element to swap.
|
||||
* @param {number} j The index of the second element to swap.
|
||||
* @private
|
||||
*/
|
||||
_swap(i, j) {
|
||||
const temp = this._heap[i];
|
||||
this._heap[i] = this._heap[j];
|
||||
this._heap[j] = temp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maintain the heap property by updating positions in the heap,
|
||||
* starting at the last element and moving up the heap.
|
||||
* @private
|
||||
*/
|
||||
_siftUp() {
|
||||
let node = this.size - 1;
|
||||
while (node > 0 && this._greater(node, this._parent(node))) {
|
||||
this._swap(node, this._parent(node));
|
||||
node = this._parent(node);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Maintain the heap property by updating positions in the heap,
|
||||
* starting at the first element and moving down the heap.
|
||||
* @private
|
||||
*/
|
||||
_siftDown() {
|
||||
let node = 0;
|
||||
while (
|
||||
(this._left(node) < this.size && this._greater(this._left(node), node)) ||
|
||||
(this._right(node) < this.size && this._greater(this._right(node), node))
|
||||
) {
|
||||
const maxChild = (this._right(node) < this.size && this._greater(this._right(node), this._left(node)))
|
||||
? this._right(node)
|
||||
: this._left(node);
|
||||
this._swap(node, maxChild);
|
||||
node = maxChild;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A trie structure to efficiently store and search for strings.
|
||||
*/
|
||||
export class CharTrie {
|
||||
constructor() {
|
||||
this.root = CharTrieNode.default();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds one or more `texts` to the trie.
|
||||
* @param {string[]} texts The strings to add to the trie.
|
||||
*/
|
||||
extend(texts) {
|
||||
for (let text of texts) {
|
||||
this.push(text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds text to the trie.
|
||||
* @param {string} text The string to add to the trie.
|
||||
*/
|
||||
push(text) {
|
||||
let node = this.root;
|
||||
for (let ch of text) {
|
||||
let child = node.children.get(ch);
|
||||
if (child === undefined) {
|
||||
child = CharTrieNode.default();
|
||||
node.children.set(ch, child);
|
||||
}
|
||||
node = child;
|
||||
}
|
||||
node.isLeaf = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches the trie for all strings with a common prefix of `text`.
|
||||
* @param {string} text The common prefix to search for.
|
||||
* @yields {string} Each string in the trie that has `text` as a prefix.
|
||||
*/
|
||||
*commonPrefixSearch(text) {
|
||||
let node = this.root;
|
||||
let prefix = "";
|
||||
for (let i = 0; i < text.length && node !== undefined; ++i) {
|
||||
const ch = text[i];
|
||||
prefix += ch;
|
||||
node = node.children.get(ch);
|
||||
if (node !== undefined && node.isLeaf) {
|
||||
yield prefix;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a node in a character trie.
|
||||
*/
|
||||
class CharTrieNode {
|
||||
/**
|
||||
* Create a new CharTrieNode.
|
||||
* @param {boolean} isLeaf Whether the node is a leaf node or not.
|
||||
* @param {Map<string, CharTrieNode>} children A map containing the node's children, where the key is a character and the value is a `CharTrieNode`.
|
||||
*/
|
||||
constructor(isLeaf, children) {
|
||||
this.isLeaf = isLeaf;
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new `CharTrieNode` instance with default values.
|
||||
* @returns {CharTrieNode} A new `CharTrieNode` instance with `isLeaf` set to `false` and an empty `children` map.
|
||||
*/
|
||||
static default() {
|
||||
return new CharTrieNode(false, new Map());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A lattice data structure to be used for tokenization.
|
||||
*/
|
||||
export class TokenLattice {
|
||||
/**
|
||||
* Creates a new TokenLattice instance.
|
||||
*
|
||||
* @param {string} sentence The input sentence to be tokenized.
|
||||
* @param {number} bosTokenId The beginning-of-sequence token ID.
|
||||
* @param {number} eosTokenId The end-of-sequence token ID.
|
||||
*/
|
||||
constructor(sentence, bosTokenId, eosTokenId) {
|
||||
this.sentence = sentence;
|
||||
this.len = sentence.length;
|
||||
this.bosTokenId = bosTokenId;
|
||||
this.eosTokenId = eosTokenId;
|
||||
this.nodes = [];
|
||||
this.beginNodes = Array.from({ length: this.len + 1 }, () => []);
|
||||
this.endNodes = Array.from({ length: this.len + 1 }, () => []);
|
||||
|
||||
const bos = new TokenLatticeNode(this.bosTokenId, 0, 0, 0, 0.0);
|
||||
const eos = new TokenLatticeNode(this.eosTokenId, 1, this.len, 0, 0.0);
|
||||
this.nodes.push(bos.clone());
|
||||
this.nodes.push(eos.clone());
|
||||
this.beginNodes[this.len].push(eos);
|
||||
this.endNodes[0].push(bos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new token node into the token lattice.
|
||||
*
|
||||
* @param {number} pos The starting position of the token.
|
||||
* @param {number} length The length of the token.
|
||||
* @param {number} score The score of the token.
|
||||
* @param {number} tokenId The token ID of the token.
|
||||
*/
|
||||
insert(pos, length, score, tokenId) {
|
||||
const nodeId = this.nodes.length;
|
||||
const node = new TokenLatticeNode(tokenId, nodeId, pos, length, score);
|
||||
this.beginNodes[pos].push(node);
|
||||
this.endNodes[pos + length].push(node);
|
||||
this.nodes.push(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the Viterbi algorithm to compute the most likely sequence of tokens.
|
||||
*
|
||||
* @returns {TokenLatticeNode[]} The array of nodes representing the most likely sequence of tokens.
|
||||
*/
|
||||
viterbi() {
|
||||
const len = this.len;
|
||||
let pos = 0;
|
||||
while (pos <= len) {
|
||||
if (this.beginNodes[pos].length == 0) {
|
||||
return [];
|
||||
}
|
||||
for (let rnode of this.beginNodes[pos]) {
|
||||
rnode.prev = null;
|
||||
let bestScore = 0.0;
|
||||
let bestNode = null;
|
||||
for (let lnode of this.endNodes[pos]) {
|
||||
const score = lnode.backtraceScore + rnode.score;
|
||||
if (bestNode === null || score > bestScore) {
|
||||
bestNode = lnode.clone();
|
||||
bestScore = score;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestNode !== null) {
|
||||
rnode.prev = bestNode;
|
||||
rnode.backtraceScore = bestScore;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
++pos;
|
||||
}
|
||||
|
||||
const results = [];
|
||||
const root = this.beginNodes[len][0];
|
||||
const prev = root.prev;
|
||||
if (prev === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let node = prev.clone();
|
||||
while (node.prev !== null) {
|
||||
results.push(node.clone());
|
||||
const n = node.clone();
|
||||
node = n.prev.clone();
|
||||
}
|
||||
|
||||
results.reverse();
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TokenLatticeNode} node
|
||||
* @returns {string} The array of nodes representing the most likely sequence of tokens.
|
||||
*/
|
||||
piece(node) {
|
||||
return this.sentence.slice(node.pos, node.pos + node.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Array} The array of nodes representing the most likely sequence of tokens.
|
||||
*/
|
||||
tokens() {
|
||||
const nodes = this.viterbi();
|
||||
return nodes.map(x => this.piece(x));
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Array} The array of nodes representing the most likely sequence of tokens.
|
||||
*/
|
||||
tokenIds() {
|
||||
const nodes = this.viterbi();
|
||||
return nodes.map(x => x.tokenId);
|
||||
}
|
||||
}
|
||||
class TokenLatticeNode {
|
||||
/**
|
||||
* Represents a node in a token lattice for a given sentence.
|
||||
* @param {number} tokenId The ID of the token associated with this node.
|
||||
* @param {number} nodeId The ID of this node.
|
||||
* @param {number} pos The starting position of the token in the sentence.
|
||||
* @param {number} length The length of the token.
|
||||
* @param {number} score The score associated with the token.
|
||||
*/
|
||||
constructor(tokenId, nodeId, pos, length, score) {
|
||||
this.tokenId = tokenId;
|
||||
this.nodeId = nodeId;
|
||||
this.pos = pos;
|
||||
this.length = length;
|
||||
this.score = score;
|
||||
this.prev = null;
|
||||
this.backtraceScore = 0.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a clone of this node.
|
||||
* @returns {TokenLatticeNode} A clone of this node.
|
||||
*/
|
||||
clone() {
|
||||
const n = new TokenLatticeNode(this.tokenId, this.nodeId, this.pos, this.length, this.score);
|
||||
n.prev = this.prev;
|
||||
n.backtraceScore = this.backtraceScore;
|
||||
return n;
|
||||
}
|
||||
}
|
||||
873
node_modules/@xenova/transformers/src/utils/generation.js
generated
vendored
Normal file
873
node_modules/@xenova/transformers/src/utils/generation.js
generated
vendored
Normal file
@@ -0,0 +1,873 @@
|
||||
|
||||
/**
|
||||
* @file Classes, functions, and utilities for generation.
|
||||
*
|
||||
* @todo Describe how to create a custom `GenerationConfig`.
|
||||
*
|
||||
* @module utils/generation
|
||||
*/
|
||||
import { Tensor } from './tensor.js';
|
||||
import {
|
||||
Callable,
|
||||
exists,
|
||||
} from './core.js';
|
||||
import {
|
||||
max,
|
||||
softmax,
|
||||
log_softmax,
|
||||
getTopItems,
|
||||
} from './maths.js';
|
||||
|
||||
/**
|
||||
* A class representing a list of logits processors. A logits processor is a function that modifies the logits
|
||||
* output of a language model. This class provides methods for adding new processors and applying all processors to a
|
||||
* batch of logits.
|
||||
*
|
||||
* @extends Callable
|
||||
*/
|
||||
export class LogitsProcessorList extends Callable {
|
||||
/**
|
||||
* Constructs a new instance of `LogitsProcessorList`.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this.processors = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new logits processor to the list.
|
||||
*
|
||||
* @param {LogitsProcessor} item The logits processor function to add.
|
||||
*/
|
||||
push(item) {
|
||||
this.processors.push(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds multiple logits processors to the list.
|
||||
*
|
||||
* @param {LogitsProcessor[]} items The logits processor functions to add.
|
||||
*/
|
||||
extend(items) {
|
||||
this.processors.push(...items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies all logits processors in the list to a batch of logits, modifying them in-place.
|
||||
*
|
||||
* @param {number[]} input_ids The input IDs for the language model.
|
||||
* @param {number[][]} batchedLogits A 2D array of logits, where each row corresponds to a single
|
||||
* input sequence in the batch.
|
||||
*/
|
||||
_call(input_ids, batchedLogits) {
|
||||
// NOTE: This is different from the Python code, since vanilla JS does not support vectorized operations.
|
||||
// As a result, we apply each processor to each item in the batch.
|
||||
for (let logits of batchedLogits) {
|
||||
// Modifies logits inplace
|
||||
this.processors.forEach(
|
||||
func => func(input_ids, logits)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this.processors.values();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for processing logits.
|
||||
* @extends Callable
|
||||
*/
|
||||
export class LogitsProcessor extends Callable {
|
||||
/**
|
||||
* Apply the processor to the input logits.
|
||||
*
|
||||
* @abstract
|
||||
* @param {Array} input_ids The input ids.
|
||||
* @param {Tensor} logits The logits to process.
|
||||
* @throws {Error} Throws an error if `_call` is not implemented in the subclass.
|
||||
*/
|
||||
_call(input_ids, logits) {
|
||||
throw Error("`_call` should be implemented in a subclass")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A logits processor that forces a specific token to be generated by the decoder.
|
||||
*
|
||||
* @extends LogitsProcessor
|
||||
*/
|
||||
export class ForceTokensLogitsProcessor extends LogitsProcessor {
|
||||
/**
|
||||
* Constructs a new instance of `ForceTokensLogitsProcessor`.
|
||||
*
|
||||
* @param {Array} forced_decoder_ids The ids of tokens that should be forced.
|
||||
*/
|
||||
constructor(forced_decoder_ids) {
|
||||
super();
|
||||
this.force_token_map = Object.fromEntries(forced_decoder_ids ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the processor to the input logits.
|
||||
*
|
||||
* @param {Array} input_ids The input ids.
|
||||
* @param {Tensor} logits The logits to process.
|
||||
* @returns {Tensor} The processed logits.
|
||||
*/
|
||||
_call(input_ids, logits) {
|
||||
let map = this.force_token_map[input_ids.length];
|
||||
if (exists(map)) { // There exists a mapping
|
||||
logits.data.fill(-Infinity)
|
||||
logits.data[map] = 0;
|
||||
}
|
||||
return logits;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A LogitsProcessor that forces a BOS token at the beginning of the generated sequence.
|
||||
* @extends LogitsProcessor
|
||||
*/
|
||||
export class ForcedBOSTokenLogitsProcessor extends LogitsProcessor {
|
||||
/**
|
||||
* Create a ForcedBOSTokenLogitsProcessor.
|
||||
* @param {number} bos_token_id The ID of the beginning-of-sequence token to be forced.
|
||||
*/
|
||||
constructor(bos_token_id) {
|
||||
super();
|
||||
this.bos_token_id = bos_token_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the BOS token forcing to the logits.
|
||||
* @param {Array} input_ids The input IDs.
|
||||
* @param {Object} logits The logits.
|
||||
* @returns {Object} The logits with BOS token forcing.
|
||||
*/
|
||||
_call(input_ids, logits) {
|
||||
if (input_ids.length === 1) {
|
||||
logits.data.fill(-Infinity)
|
||||
logits.data[this.bos_token_id] = 0;
|
||||
}
|
||||
return logits;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A logits processor that forces end-of-sequence token probability to 1.
|
||||
*
|
||||
* @extends LogitsProcessor
|
||||
*/
|
||||
export class ForcedEOSTokenLogitsProcessor extends LogitsProcessor {
|
||||
/**
|
||||
* Create a ForcedEOSTokenLogitsProcessor.
|
||||
* @param {number} max_length Max length of the sequence.
|
||||
* @param {number|number[]} forced_eos_token_id The ID of the end-of-sequence token to be forced.
|
||||
*/
|
||||
constructor(max_length, forced_eos_token_id) {
|
||||
super();
|
||||
this.max_length = max_length;
|
||||
this.forced_eos_token_id = forced_eos_token_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the processor to input_ids and logits.
|
||||
*
|
||||
* @param {number[]} input_ids The input ids.
|
||||
* @param {Tensor} logits The logits tensor.
|
||||
*/
|
||||
_call(input_ids, logits) {
|
||||
// console.log('call ForcedEOSTokenLogitsProcessor')
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A LogitsProcessor that suppresses a list of tokens as soon as the `generate` function starts
|
||||
* generating using `begin_index` tokens. This should ensure that the tokens defined by
|
||||
* `begin_suppress_tokens` at not sampled at the begining of the generation.
|
||||
* @extends LogitsProcessor
|
||||
*/
|
||||
export class SuppressTokensAtBeginLogitsProcessor extends LogitsProcessor {
|
||||
/**
|
||||
* Create a SuppressTokensAtBeginLogitsProcessor.
|
||||
* @param {number[]} begin_suppress_tokens The IDs of the tokens to suppress.
|
||||
* @param {number} begin_index The number of tokens to generate before suppressing tokens.
|
||||
*/
|
||||
constructor(begin_suppress_tokens, begin_index) {
|
||||
super();
|
||||
this.begin_suppress_tokens = begin_suppress_tokens;
|
||||
this.begin_index = begin_index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the BOS token forcing to the logits.
|
||||
* @param {Array} input_ids The input IDs.
|
||||
* @param {Object} logits The logits.
|
||||
* @returns {Object} The logits with BOS token forcing.
|
||||
*/
|
||||
_call(input_ids, logits) {
|
||||
if (input_ids.length === this.begin_index) {
|
||||
for (let token_id of this.begin_suppress_tokens) {
|
||||
logits.data[token_id] = -Infinity;
|
||||
}
|
||||
}
|
||||
return logits;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A LogitsProcessor that handles adding timestamps to generated text.
|
||||
* @extends LogitsProcessor
|
||||
*/
|
||||
export class WhisperTimeStampLogitsProcessor extends LogitsProcessor {
|
||||
/**
|
||||
* Constructs a new WhisperTimeStampLogitsProcessor.
|
||||
* @param {Object} generate_config The config object passed to the `generate()` method of a transformer model.
|
||||
* @param {number} generate_config.eos_token_id The ID of the end-of-sequence token.
|
||||
* @param {number} generate_config.no_timestamps_token_id The ID of the token used to indicate that a token should not have a timestamp.
|
||||
* @param {number[][]} [generate_config.forced_decoder_ids] An array of two-element arrays representing decoder IDs that are forced to appear in the output. The second element of each array indicates whether the token is a timestamp.
|
||||
* @param {number} [generate_config.max_initial_timestamp_index] The maximum index at which an initial timestamp can appear.
|
||||
*/
|
||||
constructor(generate_config) {
|
||||
super();
|
||||
this.eos_token_id = generate_config.eos_token_id;
|
||||
this.no_timestamps_token_id = generate_config.no_timestamps_token_id;
|
||||
this.timestamp_begin = this.no_timestamps_token_id + 1;
|
||||
|
||||
this.begin_index = (generate_config.forced_decoder_ids || []).length + 2;
|
||||
if (generate_config.forced_decoder_ids.slice(-1)[0][1] === this.no_timestamps_token_id) {
|
||||
this.begin_index -= 1;
|
||||
}
|
||||
this.max_initial_timestamp_index = generate_config.max_initial_timestamp_index;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the logits to handle timestamp tokens.
|
||||
* @param {Array} input_ids The input sequence of tokens.
|
||||
* @param {Tensor} logits The logits output by the model.
|
||||
* @returns {Tensor} The modified logits.
|
||||
*/
|
||||
_call(input_ids, logits) {
|
||||
const logitsData = /** @type {Float32Array} */(logits.data);
|
||||
|
||||
// suppress <|notimestamps|> which is handled by without_timestamps
|
||||
logitsData[this.no_timestamps_token_id] = -Infinity;
|
||||
|
||||
if (input_ids.length === this.begin_index - 1) {
|
||||
logitsData.fill(-Infinity);
|
||||
logitsData[this.timestamp_begin] = 0;
|
||||
return logits;
|
||||
}
|
||||
|
||||
// timestamps have to appear in pairs, except directly before eos_token; mask logits accordingly
|
||||
const seq = input_ids.slice(this.begin_index);
|
||||
const last_was_timestamp = seq.length >= 1 && seq[seq.length - 1] >= this.timestamp_begin;
|
||||
const penultimate_was_timestamp = seq.length < 2 || seq[seq.length - 2] >= this.timestamp_begin;
|
||||
|
||||
if (last_was_timestamp) {
|
||||
if (penultimate_was_timestamp) { // has to be non-timestamp
|
||||
logitsData.subarray(this.timestamp_begin).fill(-Infinity);
|
||||
} else { // cannot be normal text tokens
|
||||
logitsData.subarray(0, this.eos_token_id).fill(-Infinity);
|
||||
}
|
||||
}
|
||||
|
||||
// apply the `max_initial_timestamp` option
|
||||
if (input_ids.length === this.begin_index && this.max_initial_timestamp_index !== null) {
|
||||
const last_allowed = this.timestamp_begin + this.max_initial_timestamp_index;
|
||||
logitsData.subarray(last_allowed + 1).fill(-Infinity);
|
||||
}
|
||||
|
||||
// if sum of probability over timestamps is above any other token, sample timestamp
|
||||
const logprobs = log_softmax(logitsData);
|
||||
const timestamp_logprob = Math.log(logprobs.subarray(this.timestamp_begin).map(Math.exp).reduce((a, b) => a + b));
|
||||
const max_text_token_logprob = max(logprobs.subarray(0, this.timestamp_begin))[0];
|
||||
|
||||
if (timestamp_logprob > max_text_token_logprob) {
|
||||
logitsData.subarray(0, this.timestamp_begin).fill(-Infinity);
|
||||
}
|
||||
|
||||
return logits;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A logits processor that disallows ngrams of a certain size to be repeated.
|
||||
*
|
||||
* @extends LogitsProcessor
|
||||
*/
|
||||
export class NoRepeatNGramLogitsProcessor extends LogitsProcessor {
|
||||
/**
|
||||
* Create a NoRepeatNGramLogitsProcessor.
|
||||
* @param {number} no_repeat_ngram_size The no-repeat-ngram size. All ngrams of this size can only occur once.
|
||||
*/
|
||||
constructor(no_repeat_ngram_size) {
|
||||
super();
|
||||
this.no_repeat_ngram_size = no_repeat_ngram_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate n-grams from a sequence of token ids.
|
||||
* @param {number[]} prevInputIds List of previous input ids
|
||||
* @returns {Map<string, number[]>} Map of generated n-grams
|
||||
*/
|
||||
getNgrams(prevInputIds) {
|
||||
const curLen = prevInputIds.length;
|
||||
|
||||
/**@type {number[][]} */
|
||||
const ngrams = [];
|
||||
for (let j = 0; j < curLen + 1 - this.no_repeat_ngram_size; ++j) {
|
||||
const ngram = [];
|
||||
for (let k = 0; k < this.no_repeat_ngram_size; ++k) {
|
||||
ngram.push(prevInputIds[j + k]);
|
||||
}
|
||||
ngrams.push(ngram);
|
||||
}
|
||||
|
||||
/** @type {Map<string, number[]>} */
|
||||
const generatedNgram = new Map();
|
||||
for (const ngram of ngrams) {
|
||||
const prevNgram = ngram.slice(0, ngram.length - 1);
|
||||
const prevNgramKey = JSON.stringify(prevNgram);
|
||||
const prevNgramValue = generatedNgram.get(prevNgramKey) ?? [];
|
||||
prevNgramValue.push(ngram[ngram.length - 1]);
|
||||
generatedNgram.set(prevNgramKey, prevNgramValue);
|
||||
}
|
||||
return generatedNgram;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate n-grams from a sequence of token ids.
|
||||
* @param {Map<string, number[]>} bannedNgrams Map of banned n-grams
|
||||
* @param {number[]} prevInputIds List of previous input ids
|
||||
* @returns {number[]} Map of generated n-grams
|
||||
*/
|
||||
getGeneratedNgrams(bannedNgrams, prevInputIds) {
|
||||
const ngramIdx = prevInputIds.slice(prevInputIds.length + 1 - this.no_repeat_ngram_size, prevInputIds.length);
|
||||
const banned = bannedNgrams.get(JSON.stringify(ngramIdx)) ?? [];
|
||||
return banned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate banned n-gram tokens
|
||||
* @param {number[]} prevInputIds List of previous input ids
|
||||
* @returns {number[]} Map of generated n-grams
|
||||
*/
|
||||
calcBannedNgramTokens(prevInputIds) {
|
||||
const bannedTokens = [];
|
||||
if (prevInputIds.length + 1 < this.no_repeat_ngram_size) {
|
||||
// return no banned tokens if we haven't generated no_repeat_ngram_size tokens yet
|
||||
return bannedTokens;
|
||||
|
||||
} else {
|
||||
const generatedNgrams = this.getNgrams(prevInputIds);
|
||||
const bannedTokens = this.getGeneratedNgrams(generatedNgrams, prevInputIds);
|
||||
return bannedTokens;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the no-repeat-ngram processor to the logits.
|
||||
* @param {Array} input_ids The input IDs.
|
||||
* @param {Object} logits The logits.
|
||||
* @returns {Object} The logits with no-repeat-ngram processing.
|
||||
*/
|
||||
_call(input_ids, logits) {
|
||||
const bannedTokens = this.calcBannedNgramTokens(input_ids);
|
||||
|
||||
for (const token of bannedTokens) {
|
||||
logits.data[token] = -Infinity;
|
||||
}
|
||||
return logits;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A logits processor that penalises repeated output tokens.
|
||||
*
|
||||
* @extends LogitsProcessor
|
||||
*/
|
||||
export class RepetitionPenaltyLogitsProcessor extends LogitsProcessor {
|
||||
/**
|
||||
* Create a RepetitionPenaltyLogitsProcessor.
|
||||
* @param {number} penalty The penalty to apply for repeated tokens.
|
||||
*/
|
||||
constructor(penalty) {
|
||||
super();
|
||||
this.penalty = penalty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the repetition penalty to the logits.
|
||||
* @param {Array} input_ids The input IDs.
|
||||
* @param {Object} logits The logits.
|
||||
* @returns {Object} The logits with repetition penalty processing.
|
||||
*/
|
||||
_call(input_ids, logits) {
|
||||
// Modify the logits corresponding to each element in `input_ids`.
|
||||
// As a consequence, the logits corresponding to tokens that appear
|
||||
// many times in the output will be penalised more.
|
||||
for (const input_id of input_ids) {
|
||||
if (logits.data[input_id] < 0) {
|
||||
logits.data[input_id] *= this.penalty;
|
||||
} else {
|
||||
logits.data[input_id] /= this.penalty;
|
||||
}
|
||||
}
|
||||
return logits
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A logits processor that enforces a minimum number of tokens.
|
||||
*
|
||||
* @extends LogitsProcessor
|
||||
*/
|
||||
export class MinLengthLogitsProcessor extends LogitsProcessor {
|
||||
/**
|
||||
* Create a MinLengthLogitsProcessor.
|
||||
* @param {number} min_length The minimum length below which the score of `eos_token_id` is set to negative infinity.
|
||||
* @param {number|number[]} eos_token_id The ID/IDs of the end-of-sequence token.
|
||||
*/
|
||||
constructor(min_length, eos_token_id) {
|
||||
super();
|
||||
this.min_length = min_length;
|
||||
this.eos_token_id = Array.isArray(eos_token_id) ? eos_token_id : [eos_token_id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply logit processor.
|
||||
* @param {Array} input_ids The input IDs.
|
||||
* @param {Object} logits The logits.
|
||||
* @returns {Object} The processed logits.
|
||||
*/
|
||||
_call(input_ids, logits) {
|
||||
if (input_ids.length < this.min_length) {
|
||||
for (const eos_token of this.eos_token_id) {
|
||||
logits.data[eos_token] = -Infinity;
|
||||
}
|
||||
}
|
||||
|
||||
return logits
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A logits processor that enforces a minimum number of new tokens.
|
||||
*
|
||||
* @extends LogitsProcessor
|
||||
*/
|
||||
export class MinNewTokensLengthLogitsProcessor extends LogitsProcessor {
|
||||
/**
|
||||
* Create a MinNewTokensLengthLogitsProcessor.
|
||||
* @param {number} prompt_length_to_skip The input tokens length.
|
||||
* @param {number} min_new_tokens The minimum *new* tokens length below which the score of `eos_token_id` is set to negative infinity.
|
||||
* @param {number|number[]} eos_token_id The ID/IDs of the end-of-sequence token.
|
||||
*/
|
||||
constructor(prompt_length_to_skip, min_new_tokens, eos_token_id) {
|
||||
super();
|
||||
this.prompt_length_to_skip = prompt_length_to_skip;
|
||||
this.min_new_tokens = min_new_tokens;
|
||||
this.eos_token_id = Array.isArray(eos_token_id) ? eos_token_id : [eos_token_id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply logit processor.
|
||||
* @param {Array} input_ids The input IDs.
|
||||
* @param {Object} logits The logits.
|
||||
* @returns {Object} The processed logits.
|
||||
*/
|
||||
_call(input_ids, logits) {
|
||||
const new_tokens_length = input_ids.length - this.prompt_length_to_skip;
|
||||
if (new_tokens_length < this.min_new_tokens) {
|
||||
for (const eos_token of this.eos_token_id) {
|
||||
logits.data[eos_token] = -Infinity;
|
||||
}
|
||||
}
|
||||
|
||||
return logits
|
||||
}
|
||||
}
|
||||
|
||||
export class NoBadWordsLogitsProcessor extends LogitsProcessor {
|
||||
/**
|
||||
* Create a `NoBadWordsLogitsProcessor`.
|
||||
* @param {number[][]} bad_words_ids List of list of token ids that are not allowed to be generated.
|
||||
* @param {number|number[]} eos_token_id The id of the *end-of-sequence* token. Optionally, use a list to set multiple *end-of-sequence* tokens.
|
||||
*/
|
||||
constructor(bad_words_ids, eos_token_id) {
|
||||
super();
|
||||
this.bad_words_ids = bad_words_ids;
|
||||
this.eos_token_id = Array.isArray(eos_token_id) ? eos_token_id : [eos_token_id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply logit processor.
|
||||
* @param {Array} input_ids The input IDs.
|
||||
* @param {Object} logits The logits.
|
||||
* @returns {Object} The processed logits.
|
||||
*/
|
||||
_call(input_ids, logits) {
|
||||
|
||||
for (const bad_word_ids of this.bad_words_ids) {
|
||||
// Whether to modify the logits of the last token in the bad word id sequence
|
||||
let mark = true;
|
||||
|
||||
// For each bad word in the list, if the current sequence of input ids ends with this sequence (excluding the last),
|
||||
// then we set the logits of the last bad word id to -Infinity.
|
||||
for (let i = 1; i <= bad_word_ids.length - 1 && bad_word_ids.length < input_ids.length; ++i) {
|
||||
|
||||
if (bad_word_ids.at(-i - 1) !== input_ids.at(-i)) {
|
||||
// We have found a mismatch
|
||||
mark = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (mark) {
|
||||
logits.data[bad_word_ids.at(-1)] = -Infinity;
|
||||
}
|
||||
}
|
||||
|
||||
return logits
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} GenerationConfigType The default configuration parameters.
|
||||
* @property {number} [max_length=20] The maximum length the generated tokens can have. Corresponds to the length of the input prompt + `max_new_tokens`. Its effect is overridden by `max_new_tokens`, if also set.
|
||||
* @property {number} [max_new_tokens=null] The maximum numbers of tokens to generate, ignoring the number of tokens in the prompt.
|
||||
* @property {number} [min_length=0] The minimum length of the sequence to be generated. Corresponds to the length of the input prompt + `min_new_tokens`. Its effect is overridden by `min_new_tokens`, if also set.
|
||||
* @property {number} [min_new_tokens=null] The minimum numbers of tokens to generate, ignoring the number of tokens in the prompt.
|
||||
* @property {boolean|"never"} [early_stopping=false] Controls the stopping condition for beam-based methods, like beam-search. It accepts the following values:
|
||||
* - `true`, where the generation stops as soon as there are `num_beams` complete candidates;
|
||||
* - `false`, where an heuristic is applied and the generation stops when is it very unlikely to find better candidates;
|
||||
* - `"never"`, where the beam search procedure only stops when there cannot be better candidates (canonical beam search algorithm).
|
||||
* @property {number} [max_time=null] The maximum amount of time you allow the computation to run for in seconds. Generation will still finish the current pass after allocated time has been passed.
|
||||
*
|
||||
* @property {boolean} [do_sample=false] Whether or not to use sampling; use greedy decoding otherwise.
|
||||
* @property {number} [num_beams=1] Number of beams for beam search. 1 means no beam search.
|
||||
* @property {number} [num_beam_groups=1] Number of groups to divide `num_beams` into in order to ensure diversity among different groups of beams. See [this paper](https://arxiv.org/pdf/1610.02424.pdf) for more details.
|
||||
* @property {number} [penalty_alpha=null] The values balance the model confidence and the degeneration penalty in contrastive search decoding.
|
||||
* @property {boolean} [use_cache=true] Whether or not the model should use the past last key/values attentions (if applicable to the model) to speed up decoding.
|
||||
*
|
||||
* @property {number} [temperature=1.0] The value used to modulate the next token probabilities.
|
||||
* @property {number} [top_k=50] The number of highest probability vocabulary tokens to keep for top-k-filtering.
|
||||
* @property {number} [top_p=1.0] If set to float < 1, only the smallest set of most probable tokens with probabilities that add up to `top_p` or higher are kept for generation.
|
||||
* @property {number} [typical_p=1.0] Local typicality measures how similar the conditional probability of predicting a target token next is to the expected conditional probability of predicting a random token next, given the partial text already generated. If set to float < 1, the smallest set of the most locally typical tokens with probabilities that add up to `typical_p` or higher are kept for generation. See [this paper](https://arxiv.org/pdf/2202.00666.pdf) for more details.
|
||||
* @property {number} [epsilon_cutoff=0.0] If set to float strictly between 0 and 1, only tokens with a conditional probability greater than `epsilon_cutoff` will be sampled. In the paper, suggested values range from 3e-4 to 9e-4, depending on the size of the model. See [Truncation Sampling as Language Model Desmoothing](https://arxiv.org/abs/2210.15191) for more details.
|
||||
* @property {number} [eta_cutoff=0.0] Eta sampling is a hybrid of locally typical sampling and epsilon sampling. If set to float strictly between 0 and 1, a token is only considered if it is greater than either `eta_cutoff` or `sqrt(eta_cutoff) * exp(-entropy(softmax(next_token_logits)))`. The latter term is intuitively the expected next token probability, scaled by `sqrt(eta_cutoff)`. In the paper, suggested values range from 3e-4 to 2e-3, depending on the size of the model. See [Truncation Sampling as Language Model Desmoothing](https://arxiv.org/abs/2210.15191) for more details.
|
||||
* @property {number} [diversity_penalty=0.0] This value is subtracted from a beam's score if it generates a token same as any beam from other group at a particular time. Note that `diversity_penalty` is only effective if `group beam search` is enabled.
|
||||
* @property {number} [repetition_penalty=1.0] The parameter for repetition penalty. 1.0 means no penalty. See [this paper](https://arxiv.org/pdf/1909.05858.pdf) for more details.
|
||||
* @property {number} [encoder_repetition_penalty=1.0] The paramater for encoder_repetition_penalty. An exponential penalty on sequences that are not in the original input. 1.0 means no penalty.
|
||||
* @property {number} [length_penalty=1.0] Exponential penalty to the length that is used with beam-based generation. It is applied as an exponent to the sequence length, which in turn is used to divide the score of the sequence. Since the score is the log likelihood of the sequence (i.e. negative), `length_penalty` > 0.0 promotes longer sequences, while `length_penalty` < 0.0 encourages shorter sequences.
|
||||
* @property {number} [no_repeat_ngram_size=0] If set to int > 0, all ngrams of that size can only occur once.
|
||||
* @property {number[][]} [bad_words_ids=null] List of token ids that are not allowed to be generated. In order to get the token ids of the words that should not appear in the generated text, use `(await tokenizer(bad_words, {add_prefix_space: true, add_special_tokens: false})).input_ids`.
|
||||
* @property {number[][]|number[][][]} [force_words_ids=null] List of token ids that must be generated. If given a `number[][]`, this is treated as a simple list of words that must be included, the opposite to `bad_words_ids`. If given `number[][][]`, this triggers a [disjunctive constraint](https://github.com/huggingface/transformers/issues/14081), where one can allow different forms of each word.
|
||||
* @property {boolean} [renormalize_logits=false] Whether to renormalize the logits after applying all the logits processors or warpers (including the custom ones). It's highly recommended to set this flag to `true` as the search algorithms suppose the score logits are normalized but some logit processors or warpers break the normalization.
|
||||
* @property {Object[]} [constraints=null] Custom constraints that can be added to the generation to ensure that the output will contain the use of certain tokens as defined by `Constraint` objects, in the most sensible way possible.
|
||||
*
|
||||
* @property {number} [forced_bos_token_id=null] The id of the token to force as the first generated token after the `decoder_start_token_id`. Useful for multilingual models like mBART where the first generated token needs to be the target language token.
|
||||
* @property {number|number[]} [forced_eos_token_id=null] The id of the token to force as the last generated token when `max_length` is reached. Optionally, use a list to set multiple *end-of-sequence* tokens.
|
||||
* @property {boolean} [remove_invalid_values=false] Whether to remove possible *nan* and *inf* outputs of the model to prevent the generation method to crash. Note that using `remove_invalid_values` can slow down generation.
|
||||
* @property {number[]} [exponential_decay_length_penalty=null] This Tuple adds an exponentially increasing length penalty, after a certain amount of tokens have been generated. The tuple shall consist of: `(start_index, decay_factor)` where `start_index` indicates where penalty starts and `decay_factor` represents the factor of exponential decay.
|
||||
* @property {number[]} [suppress_tokens=null] A list of tokens that will be suppressed at generation. The `SupressTokens` logit processor will set their log probs to `-inf` so that they are not sampled.
|
||||
* @property {number[]} [begin_suppress_tokens=null] A list of tokens that will be suppressed at the beginning of the generation. The `SupressBeginTokens` logit processor will set their log probs to `-inf` so that they are not sampled.
|
||||
* @property {number[][]} [forced_decoder_ids=null] A list of pairs of integers which indicates a mapping from generation indices to token indices that will be forced before sampling. For example, `[[1, 123]]` means the second generated token will always be a token of index 123.
|
||||
*
|
||||
* @property {number} [num_return_sequences=1] The number of independently computed returned sequences for each element in the batch.
|
||||
* @property {boolean} [output_attentions=false] Whether or not to return the attentions tensors of all attention layers. See `attentions` under returned tensors for more details.
|
||||
* @property {boolean} [output_hidden_states=false] Whether or not to return the hidden states of all layers. See `hidden_states` under returned tensors for more details.
|
||||
* @property {boolean} [output_scores=false] Whether or not to return the prediction scores. See `scores` under returned tensors for more details.
|
||||
* @property {boolean} [return_dict_in_generate=false] Whether or not to return a `ModelOutput` instead of a plain tuple.
|
||||
*
|
||||
* @property {number} [pad_token_id=null] The id of the *padding* token.
|
||||
* @property {number} [bos_token_id=null] The id of the *beginning-of-sequence* token.
|
||||
* @property {number|number[]} [eos_token_id=null] The id of the *end-of-sequence* token. Optionally, use a list to set multiple *end-of-sequence* tokens.
|
||||
*
|
||||
* @property {number} [encoder_no_repeat_ngram_size=0] If set to int > 0, all ngrams of that size that occur in the `encoder_input_ids` cannot occur in the `decoder_input_ids`.
|
||||
* @property {number} [decoder_start_token_id=null] If an encoder-decoder model starts decoding with a different token than *bos*, the id of that token.
|
||||
*
|
||||
* @property {Object} [generation_kwargs={}] Additional generation kwargs will be forwarded to the `generate` function of the model. Kwargs that are not present in `generate`'s signature will be used in the model forward pass.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class that holds a configuration for a generation task.
|
||||
* @type {new (kwargs?: GenerationConfigType) => GenerationConfigType}
|
||||
*/
|
||||
export const GenerationConfig = /** @type {any} */ (class {
|
||||
|
||||
/**
|
||||
* Create a new GenerationConfig object.
|
||||
* @param {GenerationConfigType} kwargs
|
||||
*/
|
||||
constructor(kwargs = {}) {
|
||||
// Parameters that control the length of the output
|
||||
this.max_length = kwargs.max_length ?? 20;
|
||||
this.max_new_tokens = kwargs.max_new_tokens ?? null;
|
||||
this.min_length = kwargs.min_length ?? 0;
|
||||
this.min_new_tokens = kwargs.min_new_tokens ?? null;
|
||||
this.early_stopping = kwargs.early_stopping ?? false;
|
||||
this.max_time = kwargs.max_time ?? null;
|
||||
|
||||
// Parameters that control the generation strategy used
|
||||
this.do_sample = kwargs.do_sample ?? false;
|
||||
this.num_beams = kwargs.num_beams ?? 1;
|
||||
this.num_beam_groups = kwargs.num_beam_groups ?? 1;
|
||||
this.penalty_alpha = kwargs.penalty_alpha ?? null;
|
||||
this.use_cache = kwargs.use_cache ?? true;
|
||||
|
||||
// Parameters for manipulation of the model output logits
|
||||
this.temperature = kwargs.temperature ?? 1.0;
|
||||
this.top_k = kwargs.top_k ?? 50;
|
||||
this.top_p = kwargs.top_p ?? 1.0;
|
||||
this.typical_p = kwargs.typical_p ?? 1.0;
|
||||
this.epsilon_cutoff = kwargs.epsilon_cutoff ?? 0.0;
|
||||
this.eta_cutoff = kwargs.eta_cutoff ?? 0.0;
|
||||
this.diversity_penalty = kwargs.diversity_penalty ?? 0.0;
|
||||
this.repetition_penalty = kwargs.repetition_penalty ?? 1.0;
|
||||
this.encoder_repetition_penalty = kwargs.encoder_repetition_penalty ?? 1.0;
|
||||
this.length_penalty = kwargs.length_penalty ?? 1.0;
|
||||
this.no_repeat_ngram_size = kwargs.no_repeat_ngram_size ?? 0;
|
||||
this.bad_words_ids = kwargs.bad_words_ids ?? null;
|
||||
this.force_words_ids = kwargs.force_words_ids ?? null;
|
||||
this.renormalize_logits = kwargs.renormalize_logits ?? false;
|
||||
this.constraints = kwargs.constraints ?? null;
|
||||
this.forced_bos_token_id = kwargs.forced_bos_token_id ?? null;
|
||||
this.forced_eos_token_id = kwargs.forced_eos_token_id ?? null;
|
||||
this.remove_invalid_values = kwargs.remove_invalid_values ?? false;
|
||||
this.exponential_decay_length_penalty = kwargs.exponential_decay_length_penalty ?? null;
|
||||
this.suppress_tokens = kwargs.suppress_tokens ?? null;
|
||||
this.begin_suppress_tokens = kwargs.begin_suppress_tokens ?? null;
|
||||
this.forced_decoder_ids = kwargs.forced_decoder_ids ?? null;
|
||||
|
||||
// Parameters that define the output variables of `generate`
|
||||
this.num_return_sequences = kwargs.num_return_sequences ?? 1;
|
||||
this.output_attentions = kwargs.output_attentions ?? false;
|
||||
this.output_hidden_states = kwargs.output_hidden_states ?? false;
|
||||
this.output_scores = kwargs.output_scores ?? false;
|
||||
this.return_dict_in_generate = kwargs.return_dict_in_generate ?? false;
|
||||
|
||||
// Special tokens that can be used at generation time
|
||||
this.pad_token_id = kwargs.pad_token_id ?? null;
|
||||
this.bos_token_id = kwargs.bos_token_id ?? null;
|
||||
this.eos_token_id = kwargs.eos_token_id ?? null;
|
||||
|
||||
// Generation parameters exclusive to encoder-decoder models
|
||||
this.encoder_no_repeat_ngram_size = kwargs.encoder_no_repeat_ngram_size ?? 0;
|
||||
this.decoder_start_token_id = kwargs.decoder_start_token_id ?? null;
|
||||
|
||||
// Wild card
|
||||
this.generation_kwargs = kwargs.generation_kwargs ?? {};
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Sampler is a base class for all sampling methods used for text generation.
|
||||
*/
|
||||
export class Sampler extends Callable {
|
||||
/**
|
||||
* Creates a new Sampler object with the specified generation config.
|
||||
* @param {GenerationConfigType} generation_config The generation config.
|
||||
*/
|
||||
constructor(generation_config) {
|
||||
super();
|
||||
this.generation_config = generation_config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the sampler, using the specified logits.
|
||||
* @param {Tensor} logits
|
||||
* @param {number} index
|
||||
* @returns {void}
|
||||
*/
|
||||
_call(logits, index = -1) {
|
||||
// Sample from logits, of dims [batch, sequence_length, vocab_size].
|
||||
// If index is specified, sample from [batch, index, vocab_size].
|
||||
return this.sample(logits, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract method for sampling the logits.
|
||||
* @param {Tensor} logits
|
||||
* @param {number} index
|
||||
* @throws {Error}
|
||||
*/
|
||||
sample(logits, index) {
|
||||
throw Error("sample should be implemented in subclasses.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the specified logits as an array, with temperature applied.
|
||||
* @param {Tensor} logits
|
||||
* @param {number} index
|
||||
* @returns {Float32Array}
|
||||
*/
|
||||
getLogits(logits, index) {
|
||||
let vocabSize = logits.dims.at(-1);
|
||||
|
||||
let logs = /** @type {Float32Array} */(logits.data);
|
||||
|
||||
if (index === -1) {
|
||||
logs = logs.slice(-vocabSize);
|
||||
} else {
|
||||
let startIndex = index * vocabSize;
|
||||
logs = logs.slice(startIndex, startIndex + vocabSize);
|
||||
}
|
||||
|
||||
// add temperature
|
||||
if (this.generation_config.temperature > 0) {
|
||||
logs = logs.map(x => x / this.generation_config.temperature)
|
||||
}
|
||||
return logs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects an item randomly based on the specified probabilities.
|
||||
* @param {Array} probabilities An array of probabilities to use for selection.
|
||||
* @returns {number} The index of the selected item.
|
||||
*/
|
||||
randomSelect(probabilities) {
|
||||
// Return index of chosen item
|
||||
let sumProbabilities = probabilities.reduce((acc, curr) => acc + curr, 0);
|
||||
|
||||
let r = Math.random() * sumProbabilities;
|
||||
for (let i = 0; i < probabilities.length; ++i) {
|
||||
r -= probabilities[i];
|
||||
if (r <= 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0; // return first (most probable) as a fallback
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Sampler object based on the specified options.
|
||||
* @param {GenerationConfigType} generation_config An object containing options for the sampler.
|
||||
* @returns {Sampler} A Sampler object.
|
||||
*/
|
||||
static getSampler(generation_config) {
|
||||
// - *greedy decoding*: `num_beams=1` and `do_sample=False`
|
||||
// - *contrastive search*: `penalty_alpha>0` and `top_k>1`
|
||||
// - *multinomial sampling*: `num_beams=1` and `do_sample=True`
|
||||
// - *beam-search decoding*: `num_beams>1` and `do_sample=False`
|
||||
// - *beam-search multinomial sampling*: `num_beams>1` and `do_sample=True`
|
||||
// - *diverse beam-search decoding*: `num_beams>1` and `num_beam_groups>1`
|
||||
// - *constrained beam-search decoding*: `constraints!=None` or `force_words_ids!=None`
|
||||
|
||||
// NOTE: beam search is implemented directly into the generation function
|
||||
if (generation_config.do_sample) {
|
||||
return new MultinomialSampler(generation_config);
|
||||
|
||||
} else if (generation_config.num_beams > 1) {
|
||||
return new BeamSearchSampler(generation_config);
|
||||
|
||||
} else {
|
||||
if (generation_config.num_return_sequences > 1) {
|
||||
throw Error(`num_return_sequences has to be 1 when doing greedy search, but is ${generation_config.num_return_sequences}.`)
|
||||
}
|
||||
return new GreedySampler(generation_config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class representing a Greedy Sampler.
|
||||
* @extends Sampler
|
||||
*/
|
||||
class GreedySampler extends Sampler {
|
||||
/**
|
||||
* Sample the maximum probability of a given logits tensor.
|
||||
* @param {Tensor} logits
|
||||
* @param {number} [index=-1]
|
||||
* @returns {Array} An array with a single tuple, containing the index of the maximum value and a meaningless score (since this is a greedy search).
|
||||
*/
|
||||
sample(logits, index = -1) {
|
||||
// NOTE: no need to do log_softmax here since we only take the maximum
|
||||
let logs = this.getLogits(logits, index);
|
||||
let argmax = max(logs)[1];
|
||||
|
||||
// Note: score is meaningless in this context, since we are performing
|
||||
// greedy search (p = 1 => log(p) = 0)
|
||||
return [
|
||||
[argmax, 0]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class representing a MultinomialSampler.
|
||||
* @extends Sampler
|
||||
*/
|
||||
class MultinomialSampler extends Sampler {
|
||||
|
||||
/**
|
||||
* Sample from the logits.
|
||||
* @param {Tensor} logits
|
||||
* @param {number} index
|
||||
* @returns {Array}
|
||||
*/
|
||||
sample(logits, index = -1) {
|
||||
let k = logits.dims.at(-1); // defaults to vocab size
|
||||
if (this.generation_config.top_k > 0) {
|
||||
k = Math.min(this.generation_config.top_k, k);
|
||||
}
|
||||
|
||||
// Get logits of nth token
|
||||
const logs = this.getLogits(logits, index);
|
||||
|
||||
// Get top k tokens
|
||||
const topLogits = getTopItems(logs, k);
|
||||
|
||||
// Compute softmax over logits
|
||||
const probabilities = softmax(topLogits.map(x => x[1]));
|
||||
|
||||
return Array.from({ length: this.generation_config.num_beams }, () => {
|
||||
const sampledIndex = this.randomSelect(probabilities);
|
||||
return [
|
||||
topLogits[sampledIndex][0], // token id
|
||||
Math.log(probabilities[sampledIndex]), // score
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class representing a BeamSearchSampler.
|
||||
* @extends Sampler
|
||||
*/
|
||||
class BeamSearchSampler extends Sampler {
|
||||
|
||||
/**
|
||||
* Sample from the logits.
|
||||
* @param {Tensor} logits
|
||||
* @param {number} index
|
||||
* @returns {Array}
|
||||
*/
|
||||
sample(logits, index = -1) {
|
||||
let k = logits.dims.at(-1); // defaults to vocab size
|
||||
if (this.generation_config.top_k > 0) {
|
||||
k = Math.min(this.generation_config.top_k, k);
|
||||
}
|
||||
|
||||
// Get logits of nth token
|
||||
const logs = this.getLogits(logits, index);
|
||||
|
||||
// Get top k tokens
|
||||
const topLogits = getTopItems(logs, k);
|
||||
|
||||
// Compute softmax over logits
|
||||
const probabilities = softmax(topLogits.map(x => x[1]));
|
||||
|
||||
return Array.from({ length: this.generation_config.num_beams }, (_, i) => {
|
||||
return [
|
||||
topLogits[i][0], // token id
|
||||
Math.log(probabilities[i]), // score
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
658
node_modules/@xenova/transformers/src/utils/hub.js
generated
vendored
Normal file
658
node_modules/@xenova/transformers/src/utils/hub.js
generated
vendored
Normal file
@@ -0,0 +1,658 @@
|
||||
|
||||
/**
|
||||
* @file Utility functions to interact with the Hugging Face Hub (https://huggingface.co/models)
|
||||
*
|
||||
* @module utils/hub
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { env } from '../env.js';
|
||||
import { dispatchCallback } from './core.js';
|
||||
|
||||
/**
|
||||
* @typedef {Object} PretrainedOptions Options for loading a pretrained model.
|
||||
* @property {boolean?} [quantized=true] Whether to load the 8-bit quantized version of the model (only applicable when loading model files).
|
||||
* @property {function} [progress_callback=null] If specified, this function will be called during model construction, to provide the user with progress updates.
|
||||
* @property {Object} [config=null] Configuration for the model to use instead of an automatically loaded configuration. Configuration can be automatically loaded when:
|
||||
* - The model is a model provided by the library (loaded with the *model id* string of a pretrained model).
|
||||
* - The model is loaded by supplying a local directory as `pretrained_model_name_or_path` and a configuration JSON file named *config.json* is found in the directory.
|
||||
* @property {string} [cache_dir=null] Path to a directory in which a downloaded pretrained model configuration should be cached if the standard cache should not be used.
|
||||
* @property {boolean} [local_files_only=false] Whether or not to only look at local files (e.g., not try downloading the model).
|
||||
* @property {string} [revision='main'] The specific model version to use. It can be a branch name, a tag name, or a commit id,
|
||||
* since we use a git-based system for storing models and other artifacts on huggingface.co, so `revision` can be any identifier allowed by git.
|
||||
* NOTE: This setting is ignored for local requests.
|
||||
* @property {string} [model_file_name=null] If specified, load the model with this name (excluding the .onnx suffix). Currently only valid for encoder- or decoder-only models.
|
||||
*/
|
||||
|
||||
class FileResponse {
|
||||
/**
|
||||
* Mapping from file extensions to MIME types.
|
||||
*/
|
||||
_CONTENT_TYPE_MAP = {
|
||||
'txt': 'text/plain',
|
||||
'html': 'text/html',
|
||||
'css': 'text/css',
|
||||
'js': 'text/javascript',
|
||||
'json': 'application/json',
|
||||
'png': 'image/png',
|
||||
'jpg': 'image/jpeg',
|
||||
'jpeg': 'image/jpeg',
|
||||
'gif': 'image/gif',
|
||||
}
|
||||
/**
|
||||
* Creates a new `FileResponse` object.
|
||||
* @param {string|URL} filePath
|
||||
*/
|
||||
constructor(filePath) {
|
||||
this.filePath = filePath;
|
||||
this.headers = new Headers();
|
||||
|
||||
this.exists = fs.existsSync(filePath);
|
||||
if (this.exists) {
|
||||
this.status = 200;
|
||||
this.statusText = 'OK';
|
||||
|
||||
let stats = fs.statSync(filePath);
|
||||
this.headers.set('content-length', stats.size.toString());
|
||||
|
||||
this.updateContentType();
|
||||
|
||||
let self = this;
|
||||
this.body = new ReadableStream({
|
||||
start(controller) {
|
||||
self.arrayBuffer().then(buffer => {
|
||||
controller.enqueue(new Uint8Array(buffer));
|
||||
controller.close();
|
||||
})
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.status = 404;
|
||||
this.statusText = 'Not Found';
|
||||
this.body = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the 'content-type' header property of the response based on the extension of
|
||||
* the file specified by the filePath property of the current object.
|
||||
* @returns {void}
|
||||
*/
|
||||
updateContentType() {
|
||||
// Set content-type header based on file extension
|
||||
const extension = this.filePath.toString().split('.').pop().toLowerCase();
|
||||
this.headers.set('content-type', this._CONTENT_TYPE_MAP[extension] ?? 'application/octet-stream');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone the current FileResponse object.
|
||||
* @returns {FileResponse} A new FileResponse object with the same properties as the current object.
|
||||
*/
|
||||
clone() {
|
||||
let response = new FileResponse(this.filePath);
|
||||
response.exists = this.exists;
|
||||
response.status = this.status;
|
||||
response.statusText = this.statusText;
|
||||
response.headers = new Headers(this.headers);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the contents of the file specified by the filePath property and returns a Promise that
|
||||
* resolves with an ArrayBuffer containing the file's contents.
|
||||
* @returns {Promise<ArrayBuffer>} A Promise that resolves with an ArrayBuffer containing the file's contents.
|
||||
* @throws {Error} If the file cannot be read.
|
||||
*/
|
||||
async arrayBuffer() {
|
||||
const data = await fs.promises.readFile(this.filePath);
|
||||
return data.buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the contents of the file specified by the filePath property and returns a Promise that
|
||||
* resolves with a Blob containing the file's contents.
|
||||
* @returns {Promise<Blob>} A Promise that resolves with a Blob containing the file's contents.
|
||||
* @throws {Error} If the file cannot be read.
|
||||
*/
|
||||
async blob() {
|
||||
const data = await fs.promises.readFile(this.filePath);
|
||||
return new Blob([data], { type: this.headers.get('content-type') });
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the contents of the file specified by the filePath property and returns a Promise that
|
||||
* resolves with a string containing the file's contents.
|
||||
* @returns {Promise<string>} A Promise that resolves with a string containing the file's contents.
|
||||
* @throws {Error} If the file cannot be read.
|
||||
*/
|
||||
async text() {
|
||||
const data = await fs.promises.readFile(this.filePath, 'utf8');
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the contents of the file specified by the filePath property and returns a Promise that
|
||||
* resolves with a parsed JavaScript object containing the file's contents.
|
||||
*
|
||||
* @returns {Promise<Object>} A Promise that resolves with a parsed JavaScript object containing the file's contents.
|
||||
* @throws {Error} If the file cannot be read.
|
||||
*/
|
||||
async json() {
|
||||
return JSON.parse(await this.text());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given string is a valid URL.
|
||||
* @param {string|URL} string The string to test for validity as an URL.
|
||||
* @param {string[]} [protocols=null] A list of valid protocols. If specified, the protocol must be in this list.
|
||||
* @param {string[]} [validHosts=null] A list of valid hostnames. If specified, the URL's hostname must be in this list.
|
||||
* @returns {boolean} True if the string is a valid URL, false otherwise.
|
||||
*/
|
||||
function isValidUrl(string, protocols = null, validHosts = null) {
|
||||
let url;
|
||||
try {
|
||||
url = new URL(string);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
if (protocols && !protocols.includes(url.protocol)) {
|
||||
return false;
|
||||
}
|
||||
if (validHosts && !validHosts.includes(url.hostname)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get a file, using either the Fetch API or FileSystem API.
|
||||
*
|
||||
* @param {URL|string} urlOrPath The URL/path of the file to get.
|
||||
* @returns {Promise<FileResponse|Response>} A promise that resolves to a FileResponse object (if the file is retrieved using the FileSystem API), or a Response object (if the file is retrieved using the Fetch API).
|
||||
*/
|
||||
export async function getFile(urlOrPath) {
|
||||
|
||||
if (env.useFS && !isValidUrl(urlOrPath, ['http:', 'https:', 'blob:'])) {
|
||||
return new FileResponse(urlOrPath);
|
||||
|
||||
} else if (typeof process !== 'undefined' && process?.release?.name === 'node') {
|
||||
const IS_CI = !!process.env?.TESTING_REMOTELY;
|
||||
const version = env.version;
|
||||
|
||||
const headers = new Headers();
|
||||
headers.set('User-Agent', `transformers.js/${version}; is_ci/${IS_CI};`);
|
||||
|
||||
// Check whether we are making a request to the Hugging Face Hub.
|
||||
const isHFURL = isValidUrl(urlOrPath, ['http:', 'https:'], ['huggingface.co', 'hf.co']);
|
||||
if (isHFURL) {
|
||||
// If an access token is present in the environment variables,
|
||||
// we add it to the request headers.
|
||||
// NOTE: We keep `HF_ACCESS_TOKEN` for backwards compatibility (as a fallback).
|
||||
const token = process.env?.HF_TOKEN ?? process.env?.HF_ACCESS_TOKEN;
|
||||
if (token) {
|
||||
headers.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
}
|
||||
return fetch(urlOrPath, { headers });
|
||||
} else {
|
||||
// Running in a browser-environment, so we use default headers
|
||||
// NOTE: We do not allow passing authorization headers in the browser,
|
||||
// since this would require exposing the token to the client.
|
||||
return fetch(urlOrPath);
|
||||
}
|
||||
}
|
||||
|
||||
const ERROR_MAPPING = {
|
||||
// 4xx errors (https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses)
|
||||
400: 'Bad request error occurred while trying to load file',
|
||||
401: 'Unauthorized access to file',
|
||||
403: 'Forbidden access to file',
|
||||
404: 'Could not locate file',
|
||||
408: 'Request timeout error occurred while trying to load file',
|
||||
|
||||
// 5xx errors (https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#server_error_responses)
|
||||
500: 'Internal server error error occurred while trying to load file',
|
||||
502: 'Bad gateway error occurred while trying to load file',
|
||||
503: 'Service unavailable error occurred while trying to load file',
|
||||
504: 'Gateway timeout error occurred while trying to load file',
|
||||
}
|
||||
/**
|
||||
* Helper method to handle fatal errors that occur while trying to load a file from the Hugging Face Hub.
|
||||
* @param {number} status The HTTP status code of the error.
|
||||
* @param {string} remoteURL The URL of the file that could not be loaded.
|
||||
* @param {boolean} fatal Whether to raise an error if the file could not be loaded.
|
||||
* @returns {null} Returns `null` if `fatal = true`.
|
||||
* @throws {Error} If `fatal = false`.
|
||||
*/
|
||||
function handleError(status, remoteURL, fatal) {
|
||||
if (!fatal) {
|
||||
// File was not loaded correctly, but it is optional.
|
||||
// TODO in future, cache the response?
|
||||
return null;
|
||||
}
|
||||
|
||||
const message = ERROR_MAPPING[status] ?? `Error (${status}) occurred while trying to load file`;
|
||||
throw Error(`${message}: "${remoteURL}".`);
|
||||
}
|
||||
|
||||
class FileCache {
|
||||
/**
|
||||
* Instantiate a `FileCache` object.
|
||||
* @param {string} path
|
||||
*/
|
||||
constructor(path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given request is in the cache.
|
||||
* @param {string} request
|
||||
* @returns {Promise<FileResponse | undefined>}
|
||||
*/
|
||||
async match(request) {
|
||||
|
||||
let filePath = path.join(this.path, request);
|
||||
let file = new FileResponse(filePath);
|
||||
|
||||
if (file.exists) {
|
||||
return file;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given response to the cache.
|
||||
* @param {string} request
|
||||
* @param {Response|FileResponse} response
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async put(request, response) {
|
||||
const buffer = Buffer.from(await response.arrayBuffer());
|
||||
|
||||
let outputPath = path.join(this.path, request);
|
||||
|
||||
try {
|
||||
await fs.promises.mkdir(path.dirname(outputPath), { recursive: true });
|
||||
await fs.promises.writeFile(outputPath, buffer);
|
||||
|
||||
} catch (err) {
|
||||
console.warn('An error occurred while writing the file to cache:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO add the rest?
|
||||
// addAll(requests: RequestInfo[]): Promise<void>;
|
||||
// delete(request: RequestInfo | URL, options?: CacheQueryOptions): Promise<boolean>;
|
||||
// keys(request?: RequestInfo | URL, options?: CacheQueryOptions): Promise<ReadonlyArray<Request>>;
|
||||
// match(request: RequestInfo | URL, options?: CacheQueryOptions): Promise<Response | undefined>;
|
||||
// matchAll(request?: RequestInfo | URL, options?: CacheQueryOptions): Promise<ReadonlyArray<Response>>;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {FileCache|Cache} cache The cache to search
|
||||
* @param {string[]} names The names of the item to search for
|
||||
* @returns {Promise<FileResponse|Response|undefined>} The item from the cache, or undefined if not found.
|
||||
*/
|
||||
async function tryCache(cache, ...names) {
|
||||
for (let name of names) {
|
||||
try {
|
||||
let result = await cache.match(name);
|
||||
if (result) return result;
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Retrieves a file from either a remote URL using the Fetch API or from the local file system using the FileSystem API.
|
||||
* If the filesystem is available and `env.useCache = true`, the file will be downloaded and cached.
|
||||
*
|
||||
* @param {string} path_or_repo_id This can be either:
|
||||
* - a string, the *model id* of a model repo on huggingface.co.
|
||||
* - a path to a *directory* potentially containing the file.
|
||||
* @param {string} filename The name of the file to locate in `path_or_repo`.
|
||||
* @param {boolean} [fatal=true] Whether to throw an error if the file is not found.
|
||||
* @param {PretrainedOptions} [options] An object containing optional parameters.
|
||||
*
|
||||
* @throws Will throw an error if the file is not found and `fatal` is true.
|
||||
* @returns {Promise} A Promise that resolves with the file content as a buffer.
|
||||
*/
|
||||
export async function getModelFile(path_or_repo_id, filename, fatal = true, options = {}) {
|
||||
|
||||
if (!env.allowLocalModels) {
|
||||
// User has disabled local models, so we just make sure other settings are correct.
|
||||
|
||||
if (options.local_files_only) {
|
||||
throw Error("Invalid configuration detected: local models are disabled (`env.allowLocalModels=false`) but you have requested to only use local models (`local_files_only=true`).")
|
||||
} else if (!env.allowRemoteModels) {
|
||||
throw Error("Invalid configuration detected: both local and remote models are disabled. Fix by setting `env.allowLocalModels` or `env.allowRemoteModels` to `true`.")
|
||||
}
|
||||
}
|
||||
|
||||
// Initiate file retrieval
|
||||
dispatchCallback(options.progress_callback, {
|
||||
status: 'initiate',
|
||||
name: path_or_repo_id,
|
||||
file: filename
|
||||
})
|
||||
|
||||
// First, check if the a caching backend is available
|
||||
// If no caching mechanism available, will download the file every time
|
||||
let cache;
|
||||
if (!cache && env.useBrowserCache) {
|
||||
if (typeof caches === 'undefined') {
|
||||
throw Error('Browser cache is not available in this environment.')
|
||||
}
|
||||
try {
|
||||
// In some cases, the browser cache may be visible, but not accessible due to security restrictions.
|
||||
// For example, when running an application in an iframe, if a user attempts to load the page in
|
||||
// incognito mode, the following error is thrown: `DOMException: Failed to execute 'open' on 'CacheStorage':
|
||||
// An attempt was made to break through the security policy of the user agent.`
|
||||
// So, instead of crashing, we just ignore the error and continue without using the cache.
|
||||
cache = await caches.open('transformers-cache');
|
||||
} catch (e) {
|
||||
console.warn('An error occurred while opening the browser cache:', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!cache && env.useFSCache) {
|
||||
// TODO throw error if not available
|
||||
|
||||
// If `cache_dir` is not specified, use the default cache directory
|
||||
cache = new FileCache(options.cache_dir ?? env.cacheDir);
|
||||
}
|
||||
|
||||
if (!cache && env.useCustomCache) {
|
||||
// Allow the user to specify a custom cache system.
|
||||
if (!env.customCache) {
|
||||
throw Error('`env.useCustomCache=true`, but `env.customCache` is not defined.')
|
||||
}
|
||||
|
||||
// Check that the required methods are defined:
|
||||
if (!env.customCache.match || !env.customCache.put) {
|
||||
throw new Error(
|
||||
"`env.customCache` must be an object which implements the `match` and `put` functions of the Web Cache API. " +
|
||||
"For more information, see https://developer.mozilla.org/en-US/docs/Web/API/Cache"
|
||||
)
|
||||
}
|
||||
cache = env.customCache;
|
||||
}
|
||||
|
||||
const revision = options.revision ?? 'main';
|
||||
|
||||
let requestURL = pathJoin(path_or_repo_id, filename);
|
||||
let localPath = pathJoin(env.localModelPath, requestURL);
|
||||
|
||||
let remoteURL = pathJoin(
|
||||
env.remoteHost,
|
||||
env.remotePathTemplate
|
||||
.replaceAll('{model}', path_or_repo_id)
|
||||
.replaceAll('{revision}', encodeURIComponent(revision)),
|
||||
filename
|
||||
);
|
||||
|
||||
// Choose cache key for filesystem cache
|
||||
// When using the main revision (default), we use the request URL as the cache key.
|
||||
// If a specific revision is requested, we account for this in the cache key.
|
||||
let fsCacheKey = revision === 'main' ? requestURL : pathJoin(path_or_repo_id, revision, filename);
|
||||
|
||||
/** @type {string} */
|
||||
let cacheKey;
|
||||
let proposedCacheKey = cache instanceof FileCache ? fsCacheKey : remoteURL;
|
||||
|
||||
// Whether to cache the final response in the end.
|
||||
let toCacheResponse = false;
|
||||
|
||||
/** @type {Response|FileResponse|undefined} */
|
||||
let response;
|
||||
|
||||
if (cache) {
|
||||
// A caching system is available, so we try to get the file from it.
|
||||
// 1. We first try to get from cache using the local path. In some environments (like deno),
|
||||
// non-URL cache keys are not allowed. In these cases, `response` will be undefined.
|
||||
// 2. If no response is found, we try to get from cache using the remote URL or file system cache.
|
||||
response = await tryCache(cache, localPath, proposedCacheKey);
|
||||
}
|
||||
|
||||
const cacheHit = response !== undefined;
|
||||
|
||||
if (response === undefined) {
|
||||
// Caching not available, or file is not cached, so we perform the request
|
||||
|
||||
if (env.allowLocalModels) {
|
||||
// Accessing local models is enabled, so we try to get the file locally.
|
||||
// If request is a valid HTTP URL, we skip the local file check. Otherwise, we try to get the file locally.
|
||||
const isURL = isValidUrl(requestURL, ['http:', 'https:']);
|
||||
if (!isURL) {
|
||||
try {
|
||||
response = await getFile(localPath);
|
||||
cacheKey = localPath; // Update the cache key to be the local path
|
||||
} catch (e) {
|
||||
// Something went wrong while trying to get the file locally.
|
||||
// NOTE: error handling is done in the next step (since `response` will be undefined)
|
||||
console.warn(`Unable to load from local path "${localPath}": "${e}"`);
|
||||
}
|
||||
} else if (options.local_files_only) {
|
||||
throw new Error(`\`local_files_only=true\`, but attempted to load a remote file from: ${requestURL}.`);
|
||||
} else if (!env.allowRemoteModels) {
|
||||
throw new Error(`\`env.allowRemoteModels=false\`, but attempted to load a remote file from: ${requestURL}.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (response === undefined || response.status === 404) {
|
||||
// File not found locally. This means either:
|
||||
// - The user has disabled local file access (`env.allowLocalModels=false`)
|
||||
// - the path is a valid HTTP url (`response === undefined`)
|
||||
// - the path is not a valid HTTP url and the file is not present on the file system or local server (`response.status === 404`)
|
||||
|
||||
if (options.local_files_only || !env.allowRemoteModels) {
|
||||
// User requested local files only, but the file is not found locally.
|
||||
if (fatal) {
|
||||
throw Error(`\`local_files_only=true\` or \`env.allowRemoteModels=false\` and file was not found locally at "${localPath}".`);
|
||||
} else {
|
||||
// File not found, but this file is optional.
|
||||
// TODO in future, cache the response?
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// File not found locally, so we try to download it from the remote server
|
||||
response = await getFile(remoteURL);
|
||||
|
||||
if (response.status !== 200) {
|
||||
return handleError(response.status, remoteURL, fatal);
|
||||
}
|
||||
|
||||
// Success! We use the proposed cache key from earlier
|
||||
cacheKey = proposedCacheKey;
|
||||
}
|
||||
|
||||
// Only cache the response if:
|
||||
toCacheResponse =
|
||||
cache // 1. A caching system is available
|
||||
&& typeof Response !== 'undefined' // 2. `Response` is defined (i.e., we are in a browser-like environment)
|
||||
&& response instanceof Response // 3. result is a `Response` object (i.e., not a `FileResponse`)
|
||||
&& response.status === 200 // 4. request was successful (status code 200)
|
||||
}
|
||||
|
||||
// Start downloading
|
||||
dispatchCallback(options.progress_callback, {
|
||||
status: 'download',
|
||||
name: path_or_repo_id,
|
||||
file: filename
|
||||
})
|
||||
|
||||
const progressInfo = {
|
||||
status: 'progress',
|
||||
name: path_or_repo_id,
|
||||
file: filename
|
||||
}
|
||||
|
||||
/** @type {Uint8Array} */
|
||||
let buffer;
|
||||
|
||||
if (!options.progress_callback) {
|
||||
// If no progress callback is specified, we can use the `.arrayBuffer()`
|
||||
// method to read the response.
|
||||
buffer = new Uint8Array(await response.arrayBuffer());
|
||||
|
||||
} else if (
|
||||
cacheHit // The item is being read from the cache
|
||||
&&
|
||||
typeof navigator !== 'undefined' && /firefox/i.test(navigator.userAgent) // We are in Firefox
|
||||
) {
|
||||
// Due to bug in Firefox, we cannot display progress when loading from cache.
|
||||
// Fortunately, since this should be instantaneous, this should not impact users too much.
|
||||
buffer = new Uint8Array(await response.arrayBuffer());
|
||||
|
||||
// For completeness, we still fire the final progress callback
|
||||
dispatchCallback(options.progress_callback, {
|
||||
...progressInfo,
|
||||
progress: 100,
|
||||
loaded: buffer.length,
|
||||
total: buffer.length,
|
||||
})
|
||||
} else {
|
||||
buffer = await readResponse(response, data => {
|
||||
dispatchCallback(options.progress_callback, {
|
||||
...progressInfo,
|
||||
...data,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
// Only cache web responses
|
||||
// i.e., do not cache FileResponses (prevents duplication)
|
||||
toCacheResponse && cacheKey
|
||||
&&
|
||||
// Check again whether request is in cache. If not, we add the response to the cache
|
||||
(await cache.match(cacheKey) === undefined)
|
||||
) {
|
||||
// NOTE: We use `new Response(buffer, ...)` instead of `response.clone()` to handle LFS files
|
||||
await cache.put(cacheKey, new Response(buffer, {
|
||||
headers: response.headers
|
||||
}))
|
||||
.catch(err => {
|
||||
// Do not crash if unable to add to cache (e.g., QuotaExceededError).
|
||||
// Rather, log a warning and proceed with execution.
|
||||
console.warn(`Unable to add response to browser cache: ${err}.`);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
dispatchCallback(options.progress_callback, {
|
||||
status: 'done',
|
||||
name: path_or_repo_id,
|
||||
file: filename
|
||||
});
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a JSON file from a given path and file name.
|
||||
*
|
||||
* @param {string} modelPath The path to the directory containing the file.
|
||||
* @param {string} fileName The name of the file to fetch.
|
||||
* @param {boolean} [fatal=true] Whether to throw an error if the file is not found.
|
||||
* @param {PretrainedOptions} [options] An object containing optional parameters.
|
||||
* @returns {Promise<Object>} The JSON data parsed into a JavaScript object.
|
||||
* @throws Will throw an error if the file is not found and `fatal` is true.
|
||||
*/
|
||||
export async function getModelJSON(modelPath, fileName, fatal = true, options = {}) {
|
||||
let buffer = await getModelFile(modelPath, fileName, fatal, options);
|
||||
if (buffer === null) {
|
||||
// Return empty object
|
||||
return {}
|
||||
}
|
||||
|
||||
let decoder = new TextDecoder('utf-8');
|
||||
let jsonData = decoder.decode(buffer);
|
||||
|
||||
return JSON.parse(jsonData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and track progress when reading a Response object
|
||||
*
|
||||
* @param {any} response The Response object to read
|
||||
* @param {function} progress_callback The function to call with progress updates
|
||||
* @returns {Promise<Uint8Array>} A Promise that resolves with the Uint8Array buffer
|
||||
*/
|
||||
async function readResponse(response, progress_callback) {
|
||||
|
||||
const contentLength = response.headers.get('Content-Length');
|
||||
if (contentLength === null) {
|
||||
console.warn('Unable to determine content-length from response headers. Will expand buffer when needed.')
|
||||
}
|
||||
let total = parseInt(contentLength ?? '0');
|
||||
let buffer = new Uint8Array(total);
|
||||
let loaded = 0;
|
||||
|
||||
const reader = response.body.getReader();
|
||||
async function read() {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) return;
|
||||
|
||||
let newLoaded = loaded + value.length;
|
||||
if (newLoaded > total) {
|
||||
total = newLoaded;
|
||||
|
||||
// Adding the new data will overflow buffer.
|
||||
// In this case, we extend the buffer
|
||||
let newBuffer = new Uint8Array(total);
|
||||
|
||||
// copy contents
|
||||
newBuffer.set(buffer);
|
||||
|
||||
buffer = newBuffer;
|
||||
}
|
||||
buffer.set(value, loaded)
|
||||
loaded = newLoaded;
|
||||
|
||||
const progress = (loaded / total) * 100;
|
||||
|
||||
// Call your function here
|
||||
progress_callback({
|
||||
progress: progress,
|
||||
loaded: loaded,
|
||||
total: total,
|
||||
})
|
||||
|
||||
return read();
|
||||
}
|
||||
|
||||
// Actually read
|
||||
await read();
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins multiple parts of a path into a single path, while handling leading and trailing slashes.
|
||||
*
|
||||
* @param {...string} parts Multiple parts of a path.
|
||||
* @returns {string} A string representing the joined path.
|
||||
*/
|
||||
function pathJoin(...parts) {
|
||||
// https://stackoverflow.com/a/55142565
|
||||
parts = parts.map((part, index) => {
|
||||
if (index) {
|
||||
part = part.replace(new RegExp('^/'), '');
|
||||
}
|
||||
if (index !== parts.length - 1) {
|
||||
part = part.replace(new RegExp('/$'), '');
|
||||
}
|
||||
return part;
|
||||
})
|
||||
return parts.join('/');
|
||||
}
|
||||
731
node_modules/@xenova/transformers/src/utils/image.js
generated
vendored
Normal file
731
node_modules/@xenova/transformers/src/utils/image.js
generated
vendored
Normal file
@@ -0,0 +1,731 @@
|
||||
|
||||
/**
|
||||
* @file Helper module for image processing.
|
||||
*
|
||||
* These functions and classes are only used internally,
|
||||
* meaning an end-user shouldn't need to access anything here.
|
||||
*
|
||||
* @module utils/image
|
||||
*/
|
||||
|
||||
import { getFile } from './hub.js';
|
||||
import { env } from '../env.js';
|
||||
import { Tensor } from './tensor.js';
|
||||
|
||||
// Will be empty (or not used) if running in browser or web-worker
|
||||
import sharp from 'sharp';
|
||||
|
||||
const BROWSER_ENV = typeof self !== 'undefined';
|
||||
const WEBWORKER_ENV = BROWSER_ENV && self.constructor.name === 'DedicatedWorkerGlobalScope';
|
||||
|
||||
let createCanvasFunction;
|
||||
let ImageDataClass;
|
||||
let loadImageFunction;
|
||||
if (BROWSER_ENV) {
|
||||
// Running in browser or web-worker
|
||||
createCanvasFunction = (/** @type {number} */ width, /** @type {number} */ height) => {
|
||||
if (!self.OffscreenCanvas) {
|
||||
throw new Error('OffscreenCanvas not supported by this browser.');
|
||||
}
|
||||
return new self.OffscreenCanvas(width, height)
|
||||
};
|
||||
loadImageFunction = self.createImageBitmap;
|
||||
ImageDataClass = self.ImageData;
|
||||
|
||||
} else if (sharp) {
|
||||
// Running in Node.js, electron, or other non-browser environment
|
||||
|
||||
loadImageFunction = async (/**@type {sharp.Sharp}*/img) => {
|
||||
const metadata = await img.metadata();
|
||||
const rawChannels = metadata.channels;
|
||||
|
||||
let { data, info } = await img.rotate().raw().toBuffer({ resolveWithObject: true });
|
||||
|
||||
const newImage = new RawImage(new Uint8ClampedArray(data), info.width, info.height, info.channels);
|
||||
if (rawChannels !== undefined && rawChannels !== info.channels) {
|
||||
// Make sure the new image has the same number of channels as the input image.
|
||||
// This is necessary for grayscale images.
|
||||
newImage.convert(rawChannels);
|
||||
}
|
||||
return newImage;
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error('Unable to load image processing library.');
|
||||
}
|
||||
|
||||
|
||||
// Defined here: https://github.com/python-pillow/Pillow/blob/a405e8406b83f8bfb8916e93971edc7407b8b1ff/src/libImaging/Imaging.h#L262-L268
|
||||
const RESAMPLING_MAPPING = {
|
||||
0: 'nearest',
|
||||
1: 'lanczos',
|
||||
2: 'bilinear',
|
||||
3: 'bicubic',
|
||||
4: 'box',
|
||||
5: 'hamming',
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping from file extensions to MIME types.
|
||||
*/
|
||||
const CONTENT_TYPE_MAP = new Map([
|
||||
['png', 'image/png'],
|
||||
['jpg', 'image/jpeg'],
|
||||
['jpeg', 'image/jpeg'],
|
||||
['gif', 'image/gif'],
|
||||
]);
|
||||
|
||||
export class RawImage {
|
||||
|
||||
/**
|
||||
* Create a new `RawImage` object.
|
||||
* @param {Uint8ClampedArray|Uint8Array} data The pixel data.
|
||||
* @param {number} width The width of the image.
|
||||
* @param {number} height The height of the image.
|
||||
* @param {1|2|3|4} channels The number of channels.
|
||||
*/
|
||||
constructor(data, width, height, channels) {
|
||||
this.data = data;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.channels = channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the image (width, height).
|
||||
* @returns {[number, number]} The size of the image (width, height).
|
||||
*/
|
||||
get size() {
|
||||
return [this.width, this.height];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for reading an image from a variety of input types.
|
||||
* @param {RawImage|string|URL} input
|
||||
* @returns The image object.
|
||||
*
|
||||
* **Example:** Read image from a URL.
|
||||
* ```javascript
|
||||
* let image = await RawImage.read('https://huggingface.co/datasets/Xenova/transformers.js-docs/resolve/main/football-match.jpg');
|
||||
* // RawImage {
|
||||
* // "data": Uint8ClampedArray [ 25, 25, 25, 19, 19, 19, ... ],
|
||||
* // "width": 800,
|
||||
* // "height": 533,
|
||||
* // "channels": 3
|
||||
* // }
|
||||
* ```
|
||||
*/
|
||||
static async read(input) {
|
||||
if (input instanceof RawImage) {
|
||||
return input;
|
||||
} else if (typeof input === 'string' || input instanceof URL) {
|
||||
return await this.fromURL(input);
|
||||
} else {
|
||||
throw new Error(`Unsupported input type: ${typeof input}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Read an image from a URL or file path.
|
||||
* @param {string|URL} url The URL or file path to read the image from.
|
||||
* @returns {Promise<RawImage>} The image object.
|
||||
*/
|
||||
static async fromURL(url) {
|
||||
let response = await getFile(url);
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Unable to read image from "${url}" (${response.status} ${response.statusText})`);
|
||||
}
|
||||
let blob = await response.blob();
|
||||
return this.fromBlob(blob);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to create a new Image from a blob.
|
||||
* @param {Blob} blob The blob to read the image from.
|
||||
* @returns {Promise<RawImage>} The image object.
|
||||
*/
|
||||
static async fromBlob(blob) {
|
||||
if (BROWSER_ENV) {
|
||||
// Running in environment with canvas
|
||||
let img = await loadImageFunction(blob);
|
||||
|
||||
const ctx = createCanvasFunction(img.width, img.height).getContext('2d');
|
||||
|
||||
// Draw image to context
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
return new this(ctx.getImageData(0, 0, img.width, img.height).data, img.width, img.height, 4);
|
||||
|
||||
} else {
|
||||
// Use sharp.js to read (and possible resize) the image.
|
||||
let img = sharp(await blob.arrayBuffer());
|
||||
|
||||
return await loadImageFunction(img);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to create a new Image from a tensor
|
||||
* @param {Tensor} tensor
|
||||
*/
|
||||
static fromTensor(tensor, channel_format = 'CHW') {
|
||||
if (tensor.dims.length !== 3) {
|
||||
throw new Error(`Tensor should have 3 dimensions, but has ${tensor.dims.length} dimensions.`);
|
||||
}
|
||||
|
||||
if (channel_format === 'CHW') {
|
||||
tensor = tensor.transpose(1, 2, 0);
|
||||
} else if (channel_format === 'HWC') {
|
||||
// Do nothing
|
||||
} else {
|
||||
throw new Error(`Unsupported channel format: ${channel_format}`);
|
||||
}
|
||||
if (!(tensor.data instanceof Uint8ClampedArray || tensor.data instanceof Uint8Array)) {
|
||||
throw new Error(`Unsupported tensor type: ${tensor.type}`);
|
||||
}
|
||||
switch (tensor.dims[2]) {
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
case 4:
|
||||
return new RawImage(tensor.data, tensor.dims[1], tensor.dims[0], tensor.dims[2]);
|
||||
default:
|
||||
throw new Error(`Unsupported number of channels: ${tensor.dims[2]}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the image to grayscale format.
|
||||
* @returns {RawImage} `this` to support chaining.
|
||||
*/
|
||||
grayscale() {
|
||||
if (this.channels === 1) {
|
||||
return this;
|
||||
}
|
||||
|
||||
let newData = new Uint8ClampedArray(this.width * this.height * 1);
|
||||
switch (this.channels) {
|
||||
case 3: // rgb to grayscale
|
||||
case 4: // rgba to grayscale
|
||||
for (let i = 0, offset = 0; i < this.data.length; i += this.channels) {
|
||||
const red = this.data[i];
|
||||
const green = this.data[i + 1];
|
||||
const blue = this.data[i + 2];
|
||||
|
||||
newData[offset++] = Math.round(0.2989 * red + 0.5870 * green + 0.1140 * blue);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Conversion failed due to unsupported number of channels: ${this.channels}`);
|
||||
}
|
||||
return this._update(newData, this.width, this.height, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the image to RGB format.
|
||||
* @returns {RawImage} `this` to support chaining.
|
||||
*/
|
||||
rgb() {
|
||||
if (this.channels === 3) {
|
||||
return this;
|
||||
}
|
||||
|
||||
let newData = new Uint8ClampedArray(this.width * this.height * 3);
|
||||
|
||||
switch (this.channels) {
|
||||
case 1: // grayscale to rgb
|
||||
for (let i = 0, offset = 0; i < this.data.length; ++i) {
|
||||
newData[offset++] = this.data[i];
|
||||
newData[offset++] = this.data[i];
|
||||
newData[offset++] = this.data[i];
|
||||
}
|
||||
break;
|
||||
case 4: // rgba to rgb
|
||||
for (let i = 0, offset = 0; i < this.data.length; i += 4) {
|
||||
newData[offset++] = this.data[i];
|
||||
newData[offset++] = this.data[i + 1];
|
||||
newData[offset++] = this.data[i + 2];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Conversion failed due to unsupported number of channels: ${this.channels}`);
|
||||
}
|
||||
return this._update(newData, this.width, this.height, 3);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the image to RGBA format.
|
||||
* @returns {RawImage} `this` to support chaining.
|
||||
*/
|
||||
rgba() {
|
||||
if (this.channels === 4) {
|
||||
return this;
|
||||
}
|
||||
|
||||
let newData = new Uint8ClampedArray(this.width * this.height * 4);
|
||||
|
||||
switch (this.channels) {
|
||||
case 1: // grayscale to rgba
|
||||
for (let i = 0, offset = 0; i < this.data.length; ++i) {
|
||||
newData[offset++] = this.data[i];
|
||||
newData[offset++] = this.data[i];
|
||||
newData[offset++] = this.data[i];
|
||||
newData[offset++] = 255;
|
||||
}
|
||||
break;
|
||||
case 3: // rgb to rgba
|
||||
for (let i = 0, offset = 0; i < this.data.length; i += 3) {
|
||||
newData[offset++] = this.data[i];
|
||||
newData[offset++] = this.data[i + 1];
|
||||
newData[offset++] = this.data[i + 2];
|
||||
newData[offset++] = 255;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Conversion failed due to unsupported number of channels: ${this.channels}`);
|
||||
}
|
||||
|
||||
return this._update(newData, this.width, this.height, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize the image to the given dimensions. This method uses the canvas API to perform the resizing.
|
||||
* @param {number} width The width of the new image.
|
||||
* @param {number} height The height of the new image.
|
||||
* @param {Object} options Additional options for resizing.
|
||||
* @param {0|1|2|3|4|5|string} [options.resample] The resampling method to use.
|
||||
* @returns {Promise<RawImage>} `this` to support chaining.
|
||||
*/
|
||||
async resize(width, height, {
|
||||
resample = 2,
|
||||
} = {}) {
|
||||
|
||||
// Ensure resample method is a string
|
||||
let resampleMethod = RESAMPLING_MAPPING[resample] ?? resample;
|
||||
|
||||
if (BROWSER_ENV) {
|
||||
// TODO use `resample` in browser environment
|
||||
|
||||
// Store number of channels before resizing
|
||||
let numChannels = this.channels;
|
||||
|
||||
// Create canvas object for this image
|
||||
let canvas = this.toCanvas();
|
||||
|
||||
// Actually perform resizing using the canvas API
|
||||
const ctx = createCanvasFunction(width, height).getContext('2d');
|
||||
|
||||
// Draw image to context, resizing in the process
|
||||
ctx.drawImage(canvas, 0, 0, width, height);
|
||||
|
||||
// Create image from the resized data
|
||||
let resizedImage = new RawImage(ctx.getImageData(0, 0, width, height).data, width, height, 4);
|
||||
|
||||
// Convert back so that image has the same number of channels as before
|
||||
return resizedImage.convert(numChannels);
|
||||
|
||||
} else {
|
||||
// Create sharp image from raw data, and resize
|
||||
let img = this.toSharp();
|
||||
|
||||
switch (resampleMethod) {
|
||||
case 'box':
|
||||
case 'hamming':
|
||||
if (resampleMethod === 'box' || resampleMethod === 'hamming') {
|
||||
console.warn(`Resampling method ${resampleMethod} is not yet supported. Using bilinear instead.`);
|
||||
resampleMethod = 'bilinear';
|
||||
}
|
||||
|
||||
case 'nearest':
|
||||
case 'bilinear':
|
||||
case 'bicubic':
|
||||
// Perform resizing using affine transform.
|
||||
// This matches how the python Pillow library does it.
|
||||
img = img.affine([width / this.width, 0, 0, height / this.height], {
|
||||
interpolator: resampleMethod
|
||||
});
|
||||
break;
|
||||
|
||||
case 'lanczos':
|
||||
// https://github.com/python-pillow/Pillow/discussions/5519
|
||||
// https://github.com/lovell/sharp/blob/main/docs/api-resize.md
|
||||
img = img.resize({
|
||||
width, height,
|
||||
fit: 'fill',
|
||||
kernel: 'lanczos3', // PIL Lanczos uses a kernel size of 3
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Resampling method ${resampleMethod} is not supported.`);
|
||||
}
|
||||
|
||||
return await loadImageFunction(img);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async pad([left, right, top, bottom]) {
|
||||
left = Math.max(left, 0);
|
||||
right = Math.max(right, 0);
|
||||
top = Math.max(top, 0);
|
||||
bottom = Math.max(bottom, 0);
|
||||
|
||||
if (left === 0 && right === 0 && top === 0 && bottom === 0) {
|
||||
// No padding needed
|
||||
return this;
|
||||
}
|
||||
|
||||
if (BROWSER_ENV) {
|
||||
// Store number of channels before padding
|
||||
let numChannels = this.channels;
|
||||
|
||||
// Create canvas object for this image
|
||||
let canvas = this.toCanvas();
|
||||
|
||||
let newWidth = this.width + left + right;
|
||||
let newHeight = this.height + top + bottom;
|
||||
|
||||
// Create a new canvas of the desired size.
|
||||
const ctx = createCanvasFunction(newWidth, newHeight).getContext('2d');
|
||||
|
||||
// Draw image to context, padding in the process
|
||||
ctx.drawImage(canvas,
|
||||
0, 0, this.width, this.height,
|
||||
left, top, newWidth, newHeight
|
||||
);
|
||||
|
||||
// Create image from the padded data
|
||||
let paddedImage = new RawImage(
|
||||
ctx.getImageData(0, 0, newWidth, newHeight).data,
|
||||
newWidth, newHeight, 4);
|
||||
|
||||
// Convert back so that image has the same number of channels as before
|
||||
return paddedImage.convert(numChannels);
|
||||
|
||||
} else {
|
||||
let img = this.toSharp().extend({ left, right, top, bottom });
|
||||
return await loadImageFunction(img);
|
||||
}
|
||||
}
|
||||
|
||||
async crop([x_min, y_min, x_max, y_max]) {
|
||||
// Ensure crop bounds are within the image
|
||||
x_min = Math.max(x_min, 0);
|
||||
y_min = Math.max(y_min, 0);
|
||||
x_max = Math.min(x_max, this.width - 1);
|
||||
y_max = Math.min(y_max, this.height - 1);
|
||||
|
||||
// Do nothing if the crop is the entire image
|
||||
if (x_min === 0 && y_min === 0 && x_max === this.width - 1 && y_max === this.height - 1) {
|
||||
return this;
|
||||
}
|
||||
|
||||
const crop_width = x_max - x_min + 1;
|
||||
const crop_height = y_max - y_min + 1;
|
||||
|
||||
if (BROWSER_ENV) {
|
||||
// Store number of channels before resizing
|
||||
const numChannels = this.channels;
|
||||
|
||||
// Create canvas object for this image
|
||||
const canvas = this.toCanvas();
|
||||
|
||||
// Create a new canvas of the desired size. This is needed since if the
|
||||
// image is too small, we need to pad it with black pixels.
|
||||
const ctx = createCanvasFunction(crop_width, crop_height).getContext('2d');
|
||||
|
||||
// Draw image to context, cropping in the process
|
||||
ctx.drawImage(canvas,
|
||||
x_min, y_min, crop_width, crop_height,
|
||||
0, 0, crop_width, crop_height
|
||||
);
|
||||
|
||||
// Create image from the resized data
|
||||
const resizedImage = new RawImage(ctx.getImageData(0, 0, crop_width, crop_height).data, crop_width, crop_height, 4);
|
||||
|
||||
// Convert back so that image has the same number of channels as before
|
||||
return resizedImage.convert(numChannels);
|
||||
|
||||
} else {
|
||||
// Create sharp image from raw data
|
||||
const img = this.toSharp().extract({
|
||||
left: x_min,
|
||||
top: y_min,
|
||||
width: crop_width,
|
||||
height: crop_height,
|
||||
});
|
||||
|
||||
return await loadImageFunction(img);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async center_crop(crop_width, crop_height) {
|
||||
// If the image is already the desired size, return it
|
||||
if (this.width === crop_width && this.height === crop_height) {
|
||||
return this;
|
||||
}
|
||||
|
||||
// Determine bounds of the image in the new canvas
|
||||
let width_offset = (this.width - crop_width) / 2;
|
||||
let height_offset = (this.height - crop_height) / 2;
|
||||
|
||||
|
||||
if (BROWSER_ENV) {
|
||||
// Store number of channels before resizing
|
||||
let numChannels = this.channels;
|
||||
|
||||
// Create canvas object for this image
|
||||
let canvas = this.toCanvas();
|
||||
|
||||
// Create a new canvas of the desired size. This is needed since if the
|
||||
// image is too small, we need to pad it with black pixels.
|
||||
const ctx = createCanvasFunction(crop_width, crop_height).getContext('2d');
|
||||
|
||||
let sourceX = 0;
|
||||
let sourceY = 0;
|
||||
let destX = 0;
|
||||
let destY = 0;
|
||||
|
||||
if (width_offset >= 0) {
|
||||
sourceX = width_offset;
|
||||
} else {
|
||||
destX = -width_offset;
|
||||
}
|
||||
|
||||
if (height_offset >= 0) {
|
||||
sourceY = height_offset;
|
||||
} else {
|
||||
destY = -height_offset;
|
||||
}
|
||||
|
||||
// Draw image to context, cropping in the process
|
||||
ctx.drawImage(canvas,
|
||||
sourceX, sourceY, crop_width, crop_height,
|
||||
destX, destY, crop_width, crop_height
|
||||
);
|
||||
|
||||
// Create image from the resized data
|
||||
let resizedImage = new RawImage(ctx.getImageData(0, 0, crop_width, crop_height).data, crop_width, crop_height, 4);
|
||||
|
||||
// Convert back so that image has the same number of channels as before
|
||||
return resizedImage.convert(numChannels);
|
||||
|
||||
} else {
|
||||
// Create sharp image from raw data
|
||||
let img = this.toSharp();
|
||||
|
||||
if (width_offset >= 0 && height_offset >= 0) {
|
||||
// Cropped image lies entirely within the original image
|
||||
img = img.extract({
|
||||
left: Math.floor(width_offset),
|
||||
top: Math.floor(height_offset),
|
||||
width: crop_width,
|
||||
height: crop_height,
|
||||
})
|
||||
} else if (width_offset <= 0 && height_offset <= 0) {
|
||||
// Cropped image lies entirely outside the original image,
|
||||
// so we add padding
|
||||
let top = Math.floor(-height_offset);
|
||||
let left = Math.floor(-width_offset);
|
||||
img = img.extend({
|
||||
top: top,
|
||||
left: left,
|
||||
|
||||
// Ensures the resulting image has the desired dimensions
|
||||
right: crop_width - this.width - left,
|
||||
bottom: crop_height - this.height - top,
|
||||
});
|
||||
} else {
|
||||
// Cropped image lies partially outside the original image.
|
||||
// We first pad, then crop.
|
||||
|
||||
let y_padding = [0, 0];
|
||||
let y_extract = 0;
|
||||
if (height_offset < 0) {
|
||||
y_padding[0] = Math.floor(-height_offset);
|
||||
y_padding[1] = crop_height - this.height - y_padding[0];
|
||||
} else {
|
||||
y_extract = Math.floor(height_offset);
|
||||
}
|
||||
|
||||
let x_padding = [0, 0];
|
||||
let x_extract = 0;
|
||||
if (width_offset < 0) {
|
||||
x_padding[0] = Math.floor(-width_offset);
|
||||
x_padding[1] = crop_width - this.width - x_padding[0];
|
||||
} else {
|
||||
x_extract = Math.floor(width_offset);
|
||||
}
|
||||
|
||||
img = img.extend({
|
||||
top: y_padding[0],
|
||||
bottom: y_padding[1],
|
||||
left: x_padding[0],
|
||||
right: x_padding[1],
|
||||
}).extract({
|
||||
left: x_extract,
|
||||
top: y_extract,
|
||||
width: crop_width,
|
||||
height: crop_height,
|
||||
})
|
||||
}
|
||||
|
||||
return await loadImageFunction(img);
|
||||
}
|
||||
}
|
||||
|
||||
async toBlob(type = 'image/png', quality = 1) {
|
||||
if (!BROWSER_ENV) {
|
||||
throw new Error('toBlob() is only supported in browser environments.')
|
||||
}
|
||||
|
||||
const canvas = this.toCanvas();
|
||||
return await canvas.convertToBlob({ type, quality });
|
||||
}
|
||||
|
||||
toTensor(channel_format = 'CHW') {
|
||||
let tensor = new Tensor(
|
||||
'uint8',
|
||||
new Uint8Array(this.data),
|
||||
[this.height, this.width, this.channels]
|
||||
);
|
||||
|
||||
if (channel_format === 'HWC') {
|
||||
// Do nothing
|
||||
} else if (channel_format === 'CHW') { // hwc -> chw
|
||||
tensor = tensor.permute(2, 0, 1);
|
||||
} else {
|
||||
throw new Error(`Unsupported channel format: ${channel_format}`);
|
||||
}
|
||||
return tensor;
|
||||
}
|
||||
|
||||
toCanvas() {
|
||||
if (!BROWSER_ENV) {
|
||||
throw new Error('toCanvas() is only supported in browser environments.')
|
||||
}
|
||||
|
||||
// Clone, and convert data to RGBA before drawing to canvas.
|
||||
// This is because the canvas API only supports RGBA
|
||||
let cloned = this.clone().rgba();
|
||||
|
||||
// Create canvas object for the cloned image
|
||||
let clonedCanvas = createCanvasFunction(cloned.width, cloned.height);
|
||||
|
||||
// Draw image to context
|
||||
let data = new ImageDataClass(cloned.data, cloned.width, cloned.height);
|
||||
clonedCanvas.getContext('2d').putImageData(data, 0, 0);
|
||||
|
||||
return clonedCanvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to update the image data.
|
||||
* @param {Uint8ClampedArray} data The new image data.
|
||||
* @param {number} width The new width of the image.
|
||||
* @param {number} height The new height of the image.
|
||||
* @param {1|2|3|4|null} [channels] The new number of channels of the image.
|
||||
* @private
|
||||
*/
|
||||
_update(data, width, height, channels = null) {
|
||||
this.data = data;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
if (channels !== null) {
|
||||
this.channels = channels;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone the image
|
||||
* @returns {RawImage} The cloned image
|
||||
*/
|
||||
clone() {
|
||||
return new RawImage(this.data.slice(), this.width, this.height, this.channels);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for converting image to have a certain number of channels
|
||||
* @param {number} numChannels The number of channels. Must be 1, 3, or 4.
|
||||
* @returns {RawImage} `this` to support chaining.
|
||||
*/
|
||||
convert(numChannels) {
|
||||
if (this.channels === numChannels) return this; // Already correct number of channels
|
||||
|
||||
switch (numChannels) {
|
||||
case 1:
|
||||
this.grayscale();
|
||||
break;
|
||||
case 3:
|
||||
this.rgb();
|
||||
break;
|
||||
case 4:
|
||||
this.rgba();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Conversion failed due to unsupported number of channels: ${this.channels}`);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the image to the given path.
|
||||
* @param {string} path The path to save the image to.
|
||||
*/
|
||||
async save(path) {
|
||||
|
||||
if (BROWSER_ENV) {
|
||||
if (WEBWORKER_ENV) {
|
||||
throw new Error('Unable to save an image from a Web Worker.')
|
||||
}
|
||||
|
||||
const extension = path.split('.').pop().toLowerCase();
|
||||
const mime = CONTENT_TYPE_MAP.get(extension) ?? 'image/png';
|
||||
|
||||
// Convert image to Blob
|
||||
const blob = await this.toBlob(mime);
|
||||
|
||||
// Convert the canvas content to a data URL
|
||||
const dataURL = URL.createObjectURL(blob);
|
||||
|
||||
// Create an anchor element with the data URL as the href attribute
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.href = dataURL;
|
||||
|
||||
// Set the download attribute to specify the desired filename for the downloaded image
|
||||
downloadLink.download = path;
|
||||
|
||||
// Trigger the download
|
||||
downloadLink.click();
|
||||
|
||||
// Clean up: remove the anchor element from the DOM
|
||||
downloadLink.remove();
|
||||
|
||||
} else if (!env.useFS) {
|
||||
throw new Error('Unable to save the image because filesystem is disabled in this environment.')
|
||||
|
||||
} else {
|
||||
const img = this.toSharp();
|
||||
return await img.toFile(path);
|
||||
}
|
||||
}
|
||||
|
||||
toSharp() {
|
||||
if (BROWSER_ENV) {
|
||||
throw new Error('toSharp() is only supported in server-side environments.')
|
||||
}
|
||||
|
||||
return sharp(this.data, {
|
||||
raw: {
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
channels: this.channels
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
985
node_modules/@xenova/transformers/src/utils/maths.js
generated
vendored
Normal file
985
node_modules/@xenova/transformers/src/utils/maths.js
generated
vendored
Normal file
@@ -0,0 +1,985 @@
|
||||
|
||||
/**
|
||||
* @file Helper module for mathematical processing.
|
||||
*
|
||||
* These functions and classes are only used internally,
|
||||
* meaning an end-user shouldn't need to access anything here.
|
||||
*
|
||||
* @module utils/maths
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array} TypedArray
|
||||
* @typedef {BigInt64Array | BigUint64Array} BigTypedArray
|
||||
* @typedef {TypedArray | BigTypedArray} AnyTypedArray
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {TypedArray} input
|
||||
*/
|
||||
export function interpolate_data(input, [in_channels, in_height, in_width], [out_height, out_width], mode = 'bilinear', align_corners = false) {
|
||||
// TODO use mode and align_corners
|
||||
|
||||
// Output image dimensions
|
||||
const x_scale = out_width / in_width;
|
||||
const y_scale = out_height / in_height;
|
||||
|
||||
// Output image
|
||||
// @ts-ignore
|
||||
const out_img = new input.constructor(out_height * out_width * in_channels);
|
||||
|
||||
// Pre-calculate strides
|
||||
const inStride = in_height * in_width;
|
||||
const outStride = out_height * out_width;
|
||||
|
||||
for (let i = 0; i < out_height; ++i) {
|
||||
for (let j = 0; j < out_width; ++j) {
|
||||
// Calculate output offset
|
||||
const outOffset = i * out_width + j;
|
||||
|
||||
// Calculate input pixel coordinates
|
||||
const x = (j + 0.5) / x_scale - 0.5;
|
||||
const y = (i + 0.5) / y_scale - 0.5;
|
||||
|
||||
// Calculate the four nearest input pixels
|
||||
// We also check if the input pixel coordinates are within the image bounds
|
||||
let x1 = Math.floor(x);
|
||||
let y1 = Math.floor(y);
|
||||
const x2 = Math.min(x1 + 1, in_width - 1);
|
||||
const y2 = Math.min(y1 + 1, in_height - 1);
|
||||
|
||||
x1 = Math.max(x1, 0);
|
||||
y1 = Math.max(y1, 0);
|
||||
|
||||
|
||||
// Calculate the fractional distances between the input pixel and the four nearest pixels
|
||||
const s = x - x1;
|
||||
const t = y - y1;
|
||||
|
||||
// Perform bilinear interpolation
|
||||
const w1 = (1 - s) * (1 - t);
|
||||
const w2 = s * (1 - t);
|
||||
const w3 = (1 - s) * t;
|
||||
const w4 = s * t;
|
||||
|
||||
// Calculate the four nearest input pixel indices
|
||||
const yStride = y1 * in_width;
|
||||
const xStride = y2 * in_width;
|
||||
const idx1 = yStride + x1;
|
||||
const idx2 = yStride + x2;
|
||||
const idx3 = xStride + x1;
|
||||
const idx4 = xStride + x2;
|
||||
|
||||
for (let k = 0; k < in_channels; ++k) {
|
||||
// Calculate channel offset
|
||||
const cOffset = k * inStride;
|
||||
|
||||
out_img[k * outStride + outOffset] =
|
||||
w1 * input[cOffset + idx1] +
|
||||
w2 * input[cOffset + idx2] +
|
||||
w3 * input[cOffset + idx3] +
|
||||
w4 * input[cOffset + idx4];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out_img;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper method to permute a `AnyTypedArray` directly
|
||||
* @template {AnyTypedArray} T
|
||||
* @param {T} array
|
||||
* @param {number[]} dims
|
||||
* @param {number[]} axes
|
||||
* @returns {[T, number[]]} The permuted array and the new shape.
|
||||
*/
|
||||
export function permute_data(array, dims, axes) {
|
||||
// Calculate the new shape of the permuted array
|
||||
// and the stride of the original array
|
||||
const shape = new Array(axes.length);
|
||||
const stride = new Array(axes.length);
|
||||
|
||||
for (let i = axes.length - 1, s = 1; i >= 0; --i) {
|
||||
stride[i] = s;
|
||||
shape[i] = dims[axes[i]];
|
||||
s *= shape[i];
|
||||
}
|
||||
|
||||
// Precompute inverse mapping of stride
|
||||
const invStride = axes.map((_, i) => stride[axes.indexOf(i)]);
|
||||
|
||||
// Create the permuted array with the new shape
|
||||
// @ts-ignore
|
||||
const permutedData = new array.constructor(array.length);
|
||||
|
||||
// Permute the original array to the new array
|
||||
for (let i = 0; i < array.length; ++i) {
|
||||
let newIndex = 0;
|
||||
for (let j = dims.length - 1, k = i; j >= 0; --j) {
|
||||
newIndex += (k % dims[j]) * invStride[j];
|
||||
k = Math.floor(k / dims[j]);
|
||||
}
|
||||
permutedData[newIndex] = array[i];
|
||||
}
|
||||
|
||||
return [permutedData, shape];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compute the softmax of an array of numbers.
|
||||
* @template {TypedArray|number[]} T
|
||||
* @param {T} arr The array of numbers to compute the softmax of.
|
||||
* @returns {T} The softmax array.
|
||||
*/
|
||||
export function softmax(arr) {
|
||||
// Compute the maximum value in the array
|
||||
const maxVal = max(arr)[0];
|
||||
|
||||
// Compute the exponentials of the array values
|
||||
const exps = arr.map(x => Math.exp(x - maxVal));
|
||||
|
||||
// Compute the sum of the exponentials
|
||||
// @ts-ignore
|
||||
const sumExps = exps.reduce((acc, val) => acc + val, 0);
|
||||
|
||||
// Compute the softmax values
|
||||
const softmaxArr = exps.map(x => x / sumExps);
|
||||
|
||||
return /** @type {T} */(softmaxArr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the logarithm of the softmax function for the input array.
|
||||
* @template {TypedArray|number[]} T
|
||||
* @param {T} arr The input array to calculate the log_softmax function for.
|
||||
* @returns {T} The resulting log_softmax array.
|
||||
*/
|
||||
export function log_softmax(arr) {
|
||||
// Compute the softmax values
|
||||
const softmaxArr = softmax(arr);
|
||||
|
||||
// Apply log formula to each element
|
||||
const logSoftmaxArr = softmaxArr.map(x => Math.log(x));
|
||||
|
||||
return /** @type {T} */(logSoftmaxArr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the dot product of two arrays.
|
||||
* @param {number[]} arr1 The first array.
|
||||
* @param {number[]} arr2 The second array.
|
||||
* @returns {number} The dot product of arr1 and arr2.
|
||||
*/
|
||||
export function dot(arr1, arr2) {
|
||||
let result = 0;
|
||||
for (let i = 0; i < arr1.length; ++i) {
|
||||
result += arr1[i] * arr2[i];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the top k items from an iterable, sorted by descending order
|
||||
* @param {any[]|TypedArray} items The items to be sorted
|
||||
* @param {number|null} [top_k=0] The number of top items to return (default: 0 = return all)
|
||||
* @returns {[number, any][]} The top k items, sorted by descending order
|
||||
*/
|
||||
export function getTopItems(items, top_k = 0) {
|
||||
// if top == 0, return all
|
||||
|
||||
items = Array.from(items)
|
||||
.map((x, i) => [i, x]) // Get indices ([index, score])
|
||||
.sort((a, b) => b[1] - a[1]) // Sort by log probabilities
|
||||
|
||||
if (top_k !== null && top_k > 0) {
|
||||
items = items.slice(0, top_k); // Get top k items
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the cosine similarity between two arrays.
|
||||
*
|
||||
* @param {number[]} arr1 The first array.
|
||||
* @param {number[]} arr2 The second array.
|
||||
* @returns {number} The cosine similarity between the two arrays.
|
||||
*/
|
||||
export function cos_sim(arr1, arr2) {
|
||||
// Calculate dot product of the two arrays
|
||||
const dotProduct = dot(arr1, arr2);
|
||||
|
||||
// Calculate the magnitude of the first array
|
||||
const magnitudeA = magnitude(arr1);
|
||||
|
||||
// Calculate the magnitude of the second array
|
||||
const magnitudeB = magnitude(arr2);
|
||||
|
||||
// Calculate the cosine similarity
|
||||
const cosineSimilarity = dotProduct / (magnitudeA * magnitudeB);
|
||||
|
||||
return cosineSimilarity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the magnitude of a given array.
|
||||
* @param {number[]} arr The array to calculate the magnitude of.
|
||||
* @returns {number} The magnitude of the array.
|
||||
*/
|
||||
export function magnitude(arr) {
|
||||
return Math.sqrt(arr.reduce((acc, val) => acc + val * val, 0));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the value and index of the minimum element in an array.
|
||||
* @param {number[]|TypedArray} arr array of numbers.
|
||||
* @returns {number[]} the value and index of the minimum element, of the form: [valueOfMin, indexOfMin]
|
||||
* @throws {Error} If array is empty.
|
||||
*/
|
||||
export function min(arr) {
|
||||
if (arr.length === 0) throw Error('Array must not be empty');
|
||||
let min = arr[0];
|
||||
let indexOfMin = 0;
|
||||
for (let i = 1; i < arr.length; ++i) {
|
||||
if (arr[i] < min) {
|
||||
min = arr[i];
|
||||
indexOfMin = i;
|
||||
}
|
||||
}
|
||||
return [min, indexOfMin];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the value and index of the maximum element in an array.
|
||||
* @param {number[]|AnyTypedArray} arr array of numbers.
|
||||
* @returns {[number, number]} the value and index of the maximum element, of the form: [valueOfMax, indexOfMax]
|
||||
* @throws {Error} If array is empty.
|
||||
*/
|
||||
export function max(arr) {
|
||||
if (arr.length === 0) throw Error('Array must not be empty');
|
||||
let max = arr[0];
|
||||
let indexOfMax = 0;
|
||||
for (let i = 1; i < arr.length; ++i) {
|
||||
if (arr[i] > max) {
|
||||
max = arr[i];
|
||||
indexOfMax = i;
|
||||
}
|
||||
}
|
||||
return [Number(max), indexOfMax];
|
||||
}
|
||||
|
||||
function isPowerOfTwo(number) {
|
||||
// Check if the number is greater than 0 and has only one bit set to 1
|
||||
return (number > 0) && ((number & (number - 1)) === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of Radix-4 FFT.
|
||||
*
|
||||
* P2FFT class provides functionality for performing Fast Fourier Transform on arrays
|
||||
* which are a power of two in length.
|
||||
* Code adapted from https://www.npmjs.com/package/fft.js
|
||||
*/
|
||||
class P2FFT {
|
||||
/**
|
||||
* @param {number} size The size of the input array. Must be a power of two larger than 1.
|
||||
* @throws {Error} FFT size must be a power of two larger than 1.
|
||||
*/
|
||||
constructor(size) {
|
||||
this.size = size | 0; // convert to a 32-bit signed integer
|
||||
if (this.size <= 1 || !isPowerOfTwo(this.size))
|
||||
throw new Error('FFT size must be a power of two larger than 1');
|
||||
|
||||
this._csize = size << 1;
|
||||
|
||||
this.table = new Float64Array(this.size * 2);
|
||||
for (let i = 0; i < this.table.length; i += 2) {
|
||||
const angle = Math.PI * i / this.size;
|
||||
this.table[i] = Math.cos(angle);
|
||||
this.table[i + 1] = -Math.sin(angle);
|
||||
}
|
||||
|
||||
// Find size's power of two
|
||||
let power = 0;
|
||||
for (let t = 1; this.size > t; t <<= 1)
|
||||
++power;
|
||||
|
||||
// Calculate initial step's width:
|
||||
// * If we are full radix-4, it is 2x smaller to give inital len=8
|
||||
// * Otherwise it is the same as `power` to give len=4
|
||||
this._width = power % 2 === 0 ? power - 1 : power;
|
||||
|
||||
// Pre-compute bit-reversal patterns
|
||||
this._bitrev = new Int32Array(1 << this._width);
|
||||
for (let j = 0; j < this._bitrev.length; ++j) {
|
||||
this._bitrev[j] = 0;
|
||||
for (let shift = 0; shift < this._width; shift += 2) {
|
||||
const revShift = this._width - shift - 2;
|
||||
this._bitrev[j] |= ((j >>> shift) & 3) << revShift;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a complex number array with size `2 * size`
|
||||
*
|
||||
* @returns {Float64Array} A complex number array with size `2 * size`
|
||||
*/
|
||||
createComplexArray() {
|
||||
return new Float64Array(this._csize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a complex number representation stored in a Float64Array to an array of real numbers.
|
||||
*
|
||||
* @param {Float64Array} complex The complex number representation to be converted.
|
||||
* @param {number[]} [storage] An optional array to store the result in.
|
||||
* @returns {number[]} An array of real numbers representing the input complex number representation.
|
||||
*/
|
||||
fromComplexArray(complex, storage) {
|
||||
const res = storage || new Array(complex.length >>> 1);
|
||||
for (let i = 0; i < complex.length; i += 2)
|
||||
res[i >>> 1] = complex[i];
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a real-valued input array to a complex-valued output array.
|
||||
* @param {Float64Array} input The real-valued input array.
|
||||
* @param {Float64Array} [storage] Optional buffer to store the output array.
|
||||
* @returns {Float64Array} The complex-valued output array.
|
||||
*/
|
||||
toComplexArray(input, storage) {
|
||||
const res = storage || this.createComplexArray();
|
||||
for (let i = 0; i < res.length; i += 2) {
|
||||
res[i] = input[i >>> 1];
|
||||
res[i + 1] = 0;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a Fast Fourier Transform (FFT) on the given input data and stores the result in the output buffer.
|
||||
*
|
||||
* @param {Float64Array} out The output buffer to store the result.
|
||||
* @param {Float64Array} data The input data to transform.
|
||||
*
|
||||
* @throws {Error} Input and output buffers must be different.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
transform(out, data) {
|
||||
if (out === data)
|
||||
throw new Error('Input and output buffers must be different');
|
||||
|
||||
this._transform4(out, data, 1 /* DONE */);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a real-valued forward FFT on the given input buffer and stores the result in the given output buffer.
|
||||
* The input buffer must contain real values only, while the output buffer will contain complex values. The input and
|
||||
* output buffers must be different.
|
||||
*
|
||||
* @param {Float64Array} out The output buffer.
|
||||
* @param {Float64Array} data The input buffer containing real values.
|
||||
*
|
||||
* @throws {Error} If the input and output buffers are the same.
|
||||
*/
|
||||
realTransform(out, data) {
|
||||
if (out === data)
|
||||
throw new Error('Input and output buffers must be different');
|
||||
|
||||
this._realTransform4(out, data, 1 /* DONE */);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an inverse FFT transformation on the given `data` array, and stores the result in `out`.
|
||||
* The `out` array must be a different buffer than the `data` array. The `out` array will contain the
|
||||
* result of the transformation. The `data` array will not be modified.
|
||||
*
|
||||
* @param {Float64Array} out The output buffer for the transformed data.
|
||||
* @param {Float64Array} data The input data to transform.
|
||||
* @throws {Error} If `out` and `data` refer to the same buffer.
|
||||
* @returns {void}
|
||||
*/
|
||||
inverseTransform(out, data) {
|
||||
if (out === data)
|
||||
throw new Error('Input and output buffers must be different');
|
||||
|
||||
this._transform4(out, data, -1 /* DONE */);
|
||||
for (let i = 0; i < out.length; ++i)
|
||||
out[i] /= this.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a radix-4 implementation of a discrete Fourier transform on a given set of data.
|
||||
*
|
||||
* @param {Float64Array} out The output buffer for the transformed data.
|
||||
* @param {Float64Array} data The input buffer of data to be transformed.
|
||||
* @param {number} inv A scaling factor to apply to the transform.
|
||||
* @returns {void}
|
||||
*/
|
||||
_transform4(out, data, inv) {
|
||||
// radix-4 implementation
|
||||
|
||||
const size = this._csize;
|
||||
|
||||
// Initial step (permute and transform)
|
||||
const width = this._width;
|
||||
let step = 1 << width;
|
||||
let len = (size / step) << 1;
|
||||
|
||||
let outOff;
|
||||
let t;
|
||||
const bitrev = this._bitrev;
|
||||
if (len === 4) {
|
||||
for (outOff = 0, t = 0; outOff < size; outOff += len, ++t) {
|
||||
const off = bitrev[t];
|
||||
this._singleTransform2(data, out, outOff, off, step);
|
||||
}
|
||||
} else {
|
||||
// len === 8
|
||||
for (outOff = 0, t = 0; outOff < size; outOff += len, ++t) {
|
||||
const off = bitrev[t];
|
||||
this._singleTransform4(data, out, outOff, off, step, inv);
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through steps in decreasing order
|
||||
const table = this.table;
|
||||
for (step >>= 2; step >= 2; step >>= 2) {
|
||||
len = (size / step) << 1;
|
||||
const quarterLen = len >>> 2;
|
||||
|
||||
// Loop through offsets in the data
|
||||
for (outOff = 0; outOff < size; outOff += len) {
|
||||
// Full case
|
||||
const limit = outOff + quarterLen - 1;
|
||||
for (let i = outOff, k = 0; i < limit; i += 2, k += step) {
|
||||
const A = i;
|
||||
const B = A + quarterLen;
|
||||
const C = B + quarterLen;
|
||||
const D = C + quarterLen;
|
||||
|
||||
// Original values
|
||||
const Ar = out[A];
|
||||
const Ai = out[A + 1];
|
||||
const Br = out[B];
|
||||
const Bi = out[B + 1];
|
||||
const Cr = out[C];
|
||||
const Ci = out[C + 1];
|
||||
const Dr = out[D];
|
||||
const Di = out[D + 1];
|
||||
|
||||
const tableBr = table[k];
|
||||
const tableBi = inv * table[k + 1];
|
||||
const MBr = Br * tableBr - Bi * tableBi;
|
||||
const MBi = Br * tableBi + Bi * tableBr;
|
||||
|
||||
const tableCr = table[2 * k];
|
||||
const tableCi = inv * table[2 * k + 1];
|
||||
const MCr = Cr * tableCr - Ci * tableCi;
|
||||
const MCi = Cr * tableCi + Ci * tableCr;
|
||||
|
||||
const tableDr = table[3 * k];
|
||||
const tableDi = inv * table[3 * k + 1];
|
||||
const MDr = Dr * tableDr - Di * tableDi;
|
||||
const MDi = Dr * tableDi + Di * tableDr;
|
||||
|
||||
// Pre-Final values
|
||||
const T0r = Ar + MCr;
|
||||
const T0i = Ai + MCi;
|
||||
const T1r = Ar - MCr;
|
||||
const T1i = Ai - MCi;
|
||||
const T2r = MBr + MDr;
|
||||
const T2i = MBi + MDi;
|
||||
const T3r = inv * (MBr - MDr);
|
||||
const T3i = inv * (MBi - MDi);
|
||||
|
||||
// Final values
|
||||
out[A] = T0r + T2r;
|
||||
out[A + 1] = T0i + T2i;
|
||||
out[B] = T1r + T3i;
|
||||
out[B + 1] = T1i - T3r;
|
||||
out[C] = T0r - T2r;
|
||||
out[C + 1] = T0i - T2i;
|
||||
out[D] = T1r - T3i;
|
||||
out[D + 1] = T1i + T3r;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a radix-2 implementation of a discrete Fourier transform on a given set of data.
|
||||
*
|
||||
* @param {Float64Array} data The input buffer of data to be transformed.
|
||||
* @param {Float64Array} out The output buffer for the transformed data.
|
||||
* @param {number} outOff The offset at which to write the output data.
|
||||
* @param {number} off The offset at which to begin reading the input data.
|
||||
* @param {number} step The step size for indexing the input data.
|
||||
* @returns {void}
|
||||
*/
|
||||
_singleTransform2(data, out, outOff, off, step) {
|
||||
// radix-2 implementation
|
||||
// NOTE: Only called for len=4
|
||||
|
||||
const evenR = data[off];
|
||||
const evenI = data[off + 1];
|
||||
const oddR = data[off + step];
|
||||
const oddI = data[off + step + 1];
|
||||
|
||||
out[outOff] = evenR + oddR;
|
||||
out[outOff + 1] = evenI + oddI;
|
||||
out[outOff + 2] = evenR - oddR;
|
||||
out[outOff + 3] = evenI - oddI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs radix-4 transformation on input data of length 8
|
||||
*
|
||||
* @param {Float64Array} data Input data array of length 8
|
||||
* @param {Float64Array} out Output data array of length 8
|
||||
* @param {number} outOff Index of output array to start writing from
|
||||
* @param {number} off Index of input array to start reading from
|
||||
* @param {number} step Step size between elements in input array
|
||||
* @param {number} inv Scaling factor for inverse transform
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_singleTransform4(data, out, outOff, off, step, inv) {
|
||||
// radix-4
|
||||
// NOTE: Only called for len=8
|
||||
const step2 = step * 2;
|
||||
const step3 = step * 3;
|
||||
|
||||
// Original values
|
||||
const Ar = data[off];
|
||||
const Ai = data[off + 1];
|
||||
const Br = data[off + step];
|
||||
const Bi = data[off + step + 1];
|
||||
const Cr = data[off + step2];
|
||||
const Ci = data[off + step2 + 1];
|
||||
const Dr = data[off + step3];
|
||||
const Di = data[off + step3 + 1];
|
||||
|
||||
// Pre-Final values
|
||||
const T0r = Ar + Cr;
|
||||
const T0i = Ai + Ci;
|
||||
const T1r = Ar - Cr;
|
||||
const T1i = Ai - Ci;
|
||||
const T2r = Br + Dr;
|
||||
const T2i = Bi + Di;
|
||||
const T3r = inv * (Br - Dr);
|
||||
const T3i = inv * (Bi - Di);
|
||||
|
||||
// Final values
|
||||
out[outOff] = T0r + T2r;
|
||||
out[outOff + 1] = T0i + T2i;
|
||||
out[outOff + 2] = T1r + T3i;
|
||||
out[outOff + 3] = T1i - T3r;
|
||||
out[outOff + 4] = T0r - T2r;
|
||||
out[outOff + 5] = T0i - T2i;
|
||||
out[outOff + 6] = T1r - T3i;
|
||||
out[outOff + 7] = T1i + T3r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Real input radix-4 implementation
|
||||
* @param {Float64Array} out Output array for the transformed data
|
||||
* @param {Float64Array} data Input array of real data to be transformed
|
||||
* @param {number} inv The scale factor used to normalize the inverse transform
|
||||
*/
|
||||
_realTransform4(out, data, inv) {
|
||||
// Real input radix-4 implementation
|
||||
const size = this._csize;
|
||||
|
||||
// Initial step (permute and transform)
|
||||
const width = this._width;
|
||||
let step = 1 << width;
|
||||
let len = (size / step) << 1;
|
||||
|
||||
let outOff;
|
||||
let t;
|
||||
const bitrev = this._bitrev;
|
||||
if (len === 4) {
|
||||
for (outOff = 0, t = 0; outOff < size; outOff += len, ++t) {
|
||||
const off = bitrev[t];
|
||||
this._singleRealTransform2(data, out, outOff, off >>> 1, step >>> 1);
|
||||
}
|
||||
} else {
|
||||
// len === 8
|
||||
for (outOff = 0, t = 0; outOff < size; outOff += len, ++t) {
|
||||
const off = bitrev[t];
|
||||
this._singleRealTransform4(data, out, outOff, off >>> 1, step >>> 1, inv);
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through steps in decreasing order
|
||||
const table = this.table;
|
||||
for (step >>= 2; step >= 2; step >>= 2) {
|
||||
len = (size / step) << 1;
|
||||
const halfLen = len >>> 1;
|
||||
const quarterLen = halfLen >>> 1;
|
||||
const hquarterLen = quarterLen >>> 1;
|
||||
|
||||
// Loop through offsets in the data
|
||||
for (outOff = 0; outOff < size; outOff += len) {
|
||||
for (let i = 0, k = 0; i <= hquarterLen; i += 2, k += step) {
|
||||
const A = outOff + i;
|
||||
const B = A + quarterLen;
|
||||
const C = B + quarterLen;
|
||||
const D = C + quarterLen;
|
||||
|
||||
// Original values
|
||||
const Ar = out[A];
|
||||
const Ai = out[A + 1];
|
||||
const Br = out[B];
|
||||
const Bi = out[B + 1];
|
||||
const Cr = out[C];
|
||||
const Ci = out[C + 1];
|
||||
const Dr = out[D];
|
||||
const Di = out[D + 1];
|
||||
|
||||
// Middle values
|
||||
const MAr = Ar;
|
||||
const MAi = Ai;
|
||||
|
||||
const tableBr = table[k];
|
||||
const tableBi = inv * table[k + 1];
|
||||
const MBr = Br * tableBr - Bi * tableBi;
|
||||
const MBi = Br * tableBi + Bi * tableBr;
|
||||
|
||||
const tableCr = table[2 * k];
|
||||
const tableCi = inv * table[2 * k + 1];
|
||||
const MCr = Cr * tableCr - Ci * tableCi;
|
||||
const MCi = Cr * tableCi + Ci * tableCr;
|
||||
|
||||
const tableDr = table[3 * k];
|
||||
const tableDi = inv * table[3 * k + 1];
|
||||
const MDr = Dr * tableDr - Di * tableDi;
|
||||
const MDi = Dr * tableDi + Di * tableDr;
|
||||
|
||||
// Pre-Final values
|
||||
const T0r = MAr + MCr;
|
||||
const T0i = MAi + MCi;
|
||||
const T1r = MAr - MCr;
|
||||
const T1i = MAi - MCi;
|
||||
const T2r = MBr + MDr;
|
||||
const T2i = MBi + MDi;
|
||||
const T3r = inv * (MBr - MDr);
|
||||
const T3i = inv * (MBi - MDi);
|
||||
|
||||
// Final values
|
||||
out[A] = T0r + T2r;
|
||||
out[A + 1] = T0i + T2i;
|
||||
out[B] = T1r + T3i;
|
||||
out[B + 1] = T1i - T3r;
|
||||
|
||||
// Output final middle point
|
||||
if (i === 0) {
|
||||
out[C] = T0r - T2r;
|
||||
out[C + 1] = T0i - T2i;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do not overwrite ourselves
|
||||
if (i === hquarterLen)
|
||||
continue;
|
||||
|
||||
const SA = outOff + quarterLen - i;
|
||||
const SB = outOff + halfLen - i;
|
||||
|
||||
out[SA] = T1r - inv * T3i;
|
||||
out[SA + 1] = -T1i - inv * T3r;
|
||||
out[SB] = T0r - inv * T2r;
|
||||
out[SB + 1] = -T0i + inv * T2i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Complete the spectrum by adding its mirrored negative frequency components.
|
||||
const half = size >>> 1;
|
||||
for (let i = 2; i < half; i += 2) {
|
||||
out[size - i] = out[i];
|
||||
out[size - i + 1] = -out[i + 1];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a single real input radix-2 transformation on the provided data
|
||||
*
|
||||
* @param {Float64Array} data The input data array
|
||||
* @param {Float64Array} out The output data array
|
||||
* @param {number} outOff The output offset
|
||||
* @param {number} off The input offset
|
||||
* @param {number} step The step
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_singleRealTransform2(data, out, outOff, off, step) {
|
||||
// radix-2 implementation
|
||||
// NOTE: Only called for len=4
|
||||
|
||||
const evenR = data[off];
|
||||
const oddR = data[off + step];
|
||||
|
||||
out[outOff] = evenR + oddR;
|
||||
out[outOff + 1] = 0;
|
||||
out[outOff + 2] = evenR - oddR;
|
||||
out[outOff + 3] = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a single real-valued transform using radix-4 algorithm.
|
||||
* This method is only called for len=8.
|
||||
*
|
||||
* @param {Float64Array} data The input data array.
|
||||
* @param {Float64Array} out The output data array.
|
||||
* @param {number} outOff The offset into the output array.
|
||||
* @param {number} off The offset into the input array.
|
||||
* @param {number} step The step size for the input array.
|
||||
* @param {number} inv The value of inverse.
|
||||
*/
|
||||
_singleRealTransform4(data, out, outOff, off, step, inv) {
|
||||
// radix-4
|
||||
// NOTE: Only called for len=8
|
||||
const step2 = step * 2;
|
||||
const step3 = step * 3;
|
||||
|
||||
// Original values
|
||||
const Ar = data[off];
|
||||
const Br = data[off + step];
|
||||
const Cr = data[off + step2];
|
||||
const Dr = data[off + step3];
|
||||
|
||||
// Pre-Final values
|
||||
const T0r = Ar + Cr;
|
||||
const T1r = Ar - Cr;
|
||||
const T2r = Br + Dr;
|
||||
const T3r = inv * (Br - Dr);
|
||||
|
||||
// Final values
|
||||
out[outOff] = T0r + T2r;
|
||||
out[outOff + 1] = 0;
|
||||
out[outOff + 2] = T1r;
|
||||
out[outOff + 3] = -T3r;
|
||||
out[outOff + 4] = T0r - T2r;
|
||||
out[outOff + 5] = 0;
|
||||
out[outOff + 6] = T1r;
|
||||
out[outOff + 7] = T3r;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NP2FFT class provides functionality for performing Fast Fourier Transform on arrays
|
||||
* which are not a power of two in length. In such cases, the chirp-z transform is used.
|
||||
*
|
||||
* For more information, see: https://math.stackexchange.com/questions/77118/non-power-of-2-ffts/77156#77156
|
||||
*/
|
||||
class NP2FFT {
|
||||
|
||||
/**
|
||||
* Constructs a new NP2FFT object.
|
||||
* @param {number} fft_length The length of the FFT
|
||||
*/
|
||||
constructor(fft_length) {
|
||||
// Helper variables
|
||||
const a = 2 * (fft_length - 1);
|
||||
const b = 2 * (2 * fft_length - 1);
|
||||
const nextP2 = 2 ** (Math.ceil(Math.log2(b)))
|
||||
this.bufferSize = nextP2;
|
||||
this._a = a;
|
||||
|
||||
// Define buffers
|
||||
// Compute chirp for transform
|
||||
const chirp = new Float64Array(b);
|
||||
const ichirp = new Float64Array(nextP2);
|
||||
this._chirpBuffer = new Float64Array(nextP2);
|
||||
this._buffer1 = new Float64Array(nextP2);
|
||||
this._buffer2 = new Float64Array(nextP2);
|
||||
this._outBuffer1 = new Float64Array(nextP2);
|
||||
this._outBuffer2 = new Float64Array(nextP2);
|
||||
|
||||
// Compute complex exponentiation
|
||||
const theta = -2 * Math.PI / fft_length;
|
||||
const baseR = Math.cos(theta);
|
||||
const baseI = Math.sin(theta);
|
||||
|
||||
// Precompute helper for chirp-z transform
|
||||
for (let i = 0; i < b >> 1; ++i) {
|
||||
// Compute complex power:
|
||||
const e = (i + 1 - fft_length) ** 2 / 2.0;
|
||||
|
||||
// Compute the modulus and argument of the result
|
||||
const result_mod = Math.sqrt(baseR ** 2 + baseI ** 2) ** e;
|
||||
const result_arg = e * Math.atan2(baseI, baseR);
|
||||
|
||||
// Convert the result back to rectangular form
|
||||
// and assign to chirp and ichirp
|
||||
const i2 = 2 * i;
|
||||
chirp[i2] = result_mod * Math.cos(result_arg);
|
||||
chirp[i2 + 1] = result_mod * Math.sin(result_arg);
|
||||
|
||||
// conjugate
|
||||
ichirp[i2] = chirp[i2];
|
||||
ichirp[i2 + 1] = - chirp[i2 + 1];
|
||||
}
|
||||
this._slicedChirpBuffer = chirp.subarray(a, b);
|
||||
|
||||
// create object to perform Fast Fourier Transforms
|
||||
// with `nextP2` complex numbers
|
||||
this._f = new P2FFT(nextP2 >> 1);
|
||||
this._f.transform(this._chirpBuffer, ichirp);
|
||||
}
|
||||
|
||||
_transform(output, input, real) {
|
||||
const ib1 = this._buffer1;
|
||||
const ib2 = this._buffer2;
|
||||
const ob2 = this._outBuffer1;
|
||||
const ob3 = this._outBuffer2;
|
||||
const cb = this._chirpBuffer;
|
||||
const sb = this._slicedChirpBuffer;
|
||||
const a = this._a;
|
||||
|
||||
if (real) {
|
||||
// Real multiplication
|
||||
for (let j = 0; j < sb.length; j += 2) {
|
||||
const j2 = j + 1
|
||||
const j3 = j >> 1;
|
||||
|
||||
const a_real = input[j3];
|
||||
ib1[j] = a_real * sb[j];
|
||||
ib1[j2] = a_real * sb[j2];
|
||||
}
|
||||
} else {
|
||||
// Complex multiplication
|
||||
for (let j = 0; j < sb.length; j += 2) {
|
||||
const j2 = j + 1
|
||||
ib1[j] = input[j] * sb[j] - input[j2] * sb[j2];
|
||||
ib1[j2] = input[j] * sb[j2] + input[j2] * sb[j];
|
||||
}
|
||||
}
|
||||
this._f.transform(ob2, ib1);
|
||||
|
||||
for (let j = 0; j < cb.length; j += 2) {
|
||||
const j2 = j + 1;
|
||||
|
||||
ib2[j] = ob2[j] * cb[j] - ob2[j2] * cb[j2];
|
||||
ib2[j2] = ob2[j] * cb[j2] + ob2[j2] * cb[j];
|
||||
}
|
||||
this._f.inverseTransform(ob3, ib2);
|
||||
|
||||
for (let j = 0; j < ob3.length; j += 2) {
|
||||
const a_real = ob3[j + a];
|
||||
const a_imag = ob3[j + a + 1];
|
||||
const b_real = sb[j];
|
||||
const b_imag = sb[j + 1];
|
||||
|
||||
output[j] = a_real * b_real - a_imag * b_imag;
|
||||
output[j + 1] = a_real * b_imag + a_imag * b_real;
|
||||
}
|
||||
}
|
||||
|
||||
transform(output, input) {
|
||||
this._transform(output, input, false);
|
||||
}
|
||||
|
||||
realTransform(output, input) {
|
||||
this._transform(output, input, true);
|
||||
}
|
||||
}
|
||||
|
||||
export class FFT {
|
||||
constructor(fft_length) {
|
||||
this.fft_length = fft_length;
|
||||
this.isPowerOfTwo = isPowerOfTwo(fft_length);
|
||||
if (this.isPowerOfTwo) {
|
||||
this.fft = new P2FFT(fft_length);
|
||||
this.outputBufferSize = 2 * fft_length;
|
||||
} else {
|
||||
this.fft = new NP2FFT(fft_length);
|
||||
this.outputBufferSize = this.fft.bufferSize;
|
||||
}
|
||||
}
|
||||
|
||||
realTransform(out, input) {
|
||||
this.fft.realTransform(out, input);
|
||||
}
|
||||
|
||||
transform(out, input) {
|
||||
this.fft.transform(out, input);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Performs median filter on the provided data. Padding is done by mirroring the data.
|
||||
* @param {AnyTypedArray} data The input array
|
||||
* @param {number} windowSize The window size
|
||||
*/
|
||||
export function medianFilter(data, windowSize) {
|
||||
|
||||
if (windowSize % 2 === 0 || windowSize <= 0) {
|
||||
throw new Error('Window size must be a positive odd number');
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const outputArray = new data.constructor(data.length);
|
||||
|
||||
// @ts-ignore
|
||||
const buffer = new data.constructor(windowSize); // Reusable array for storing values
|
||||
|
||||
const halfWindowSize = Math.floor(windowSize / 2);
|
||||
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
let valuesIndex = 0;
|
||||
|
||||
for (let j = -halfWindowSize; j <= halfWindowSize; ++j) {
|
||||
let index = i + j;
|
||||
if (index < 0) {
|
||||
index = Math.abs(index);
|
||||
} else if (index >= data.length) {
|
||||
index = 2 * (data.length - 1) - index;
|
||||
}
|
||||
|
||||
buffer[valuesIndex++] = data[index];
|
||||
}
|
||||
|
||||
buffer.sort();
|
||||
outputArray[i] = buffer[halfWindowSize];
|
||||
}
|
||||
|
||||
return outputArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to round a number to a given number of decimals
|
||||
* @param {number} num The number to round
|
||||
* @param {number} decimals The number of decimals
|
||||
* @returns {number} The rounded number
|
||||
*/
|
||||
export function round(num, decimals) {
|
||||
const pow = Math.pow(10, decimals);
|
||||
return Math.round(num * pow) / pow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to round a number to the nearest integer, with ties rounded to the nearest even number.
|
||||
* Also known as "bankers' rounding". This is the default rounding mode in python. For example:
|
||||
* 1.5 rounds to 2 and 2.5 rounds to 2.
|
||||
*
|
||||
* @param {number} x The number to round
|
||||
* @returns {number} The rounded number
|
||||
*/
|
||||
export function bankers_round(x) {
|
||||
const r = Math.round(x);
|
||||
const br = Math.abs(x) % 1 === 0.5 ? (r % 2 === 0 ? r : r - 1) : r;
|
||||
return br;
|
||||
}
|
||||
1239
node_modules/@xenova/transformers/src/utils/tensor.js
generated
vendored
Normal file
1239
node_modules/@xenova/transformers/src/utils/tensor.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user