UNPKG

@tensorflow/tfjs-converter

Version:

Tensorflow model converter for javascript

601 lines 78.2 kB
/** * @license * Copyright 2018 Google LLC. All Rights Reserved. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ============================================================================= */ import { dispose, io, Tensor, util } from '@tensorflow/tfjs-core'; import { OperationMapper } from '../operations/operation_mapper'; import { GraphExecutor } from './graph_executor'; import { ResourceManager } from './resource_manager'; // tslint:disable-next-line: no-imports-from-dist import { decodeWeightsStream } from '@tensorflow/tfjs-core/dist/io/io_utils'; export const TFHUB_SEARCH_PARAM = '?tfjs-format=file'; export const DEFAULT_MODEL_NAME = 'model.json'; /** * A `tf.GraphModel` is a directed, acyclic graph built from a * SavedModel GraphDef and allows inference execution. * * A `tf.GraphModel` can only be created by loading from a model converted from * a [TensorFlow SavedModel](https://www.tensorflow.org/guide/saved_model) using * the command line converter tool and loaded via `tf.loadGraphModel`. * * @doc {heading: 'Models', subheading: 'Classes'} */ export class GraphModel { // Returns the version information for the tensorflow model GraphDef. get modelVersion() { return this.version; } get inputNodes() { return this.executor.inputNodes; } get outputNodes() { return this.executor.outputNodes; } get inputs() { return this.executor.inputs; } get outputs() { return this.executor.outputs; } get weights() { return this.executor.weightMap; } get metadata() { return this.artifacts.userDefinedMetadata; } get modelSignature() { return this.signature; } get modelStructuredOutputKeys() { return this.structuredOutputKeys; } /** * @param modelUrl url for the model, or an `io.IOHandler`. * @param weightManifestUrl url for the weight file generated by * scripts/convert.py script. * @param requestOption options for Request, which allows to send credentials * and custom headers. * @param onProgress Optional, progress callback function, fired periodically * before the load is completed. */ constructor(modelUrl, loadOptions = {}, tfio = io) { this.modelUrl = modelUrl; this.loadOptions = loadOptions; this.version = 'n/a'; this.io = tfio; if (loadOptions == null) { this.loadOptions = {}; } this.resourceManager = new ResourceManager(); } findIOHandler() { const path = this.modelUrl; if (path.load != null) { // Path is an IO Handler. this.handler = path; } else if (this.loadOptions.requestInit != null) { this.handler = this.io.browserHTTPRequest(path, this.loadOptions); } else { const handlers = this.io.getLoadHandlers(path, this.loadOptions); if (handlers.length === 0) { // For backward compatibility: if no load handler can be found, // assume it is a relative http path. handlers.push(this.io.browserHTTPRequest(path, this.loadOptions)); } else if (handlers.length > 1) { throw new Error(`Found more than one (${handlers.length}) load handlers for ` + `URL '${[path]}'`); } this.handler = handlers[0]; } } /** * Loads the model and weight files, construct the in memory weight map and * compile the inference graph. */ load() { this.findIOHandler(); if (this.handler.load == null) { throw new Error('Cannot proceed with model loading because the IOHandler provided ' + 'does not have the `load` method implemented.'); } const loadResult = this.handler.load(); if (util.isPromise(loadResult)) { return loadResult.then(artifacts => { if (artifacts.getWeightStream == null) { return this.loadSync(artifacts); } return this.loadStreaming(artifacts); }); } return this.loadSync(loadResult); } /** * Synchronously construct the in memory weight map and * compile the inference graph. * * @doc {heading: 'Models', subheading: 'Classes', ignoreCI: true} */ loadSync(artifacts) { const weightMap = this.io.decodeWeights(artifacts.weightData, artifacts.weightSpecs); return this.loadWithWeightMap(artifacts, weightMap); } async loadStreaming(artifacts) { if (artifacts.getWeightStream == null) { throw new Error('Model artifacts missing streamWeights function'); } const weightMap = await decodeWeightsStream(artifacts.getWeightStream(), artifacts.weightSpecs); return this.loadWithWeightMap(artifacts, weightMap); } loadWithWeightMap(artifacts, weightMap) { this.artifacts = artifacts; const graph = this.artifacts.modelTopology; let signature = this.artifacts.signature; if (this.artifacts.userDefinedMetadata != null) { const metadata = this.artifacts.userDefinedMetadata; if (metadata.signature != null) { signature = metadata.signature; } if (metadata.structuredOutputKeys != null) { this.structuredOutputKeys = metadata.structuredOutputKeys; } } this.signature = signature; this.version = `${graph.versions.producer}.${graph.versions.minConsumer}`; this.executor = new GraphExecutor(OperationMapper.Instance.transformGraph(graph, this.signature)); this.executor.weightMap = this.convertTensorMapToTensorsMap(weightMap); // Attach a model-level resourceManager to each executor to share resources, // such as `HashTable`. this.executor.resourceManager = this.resourceManager; if (artifacts.modelInitializer != null && artifacts.modelInitializer.node != null) { const initializer = OperationMapper.Instance.transformGraph(artifacts.modelInitializer); this.initializer = new GraphExecutor(initializer); this.initializer.weightMap = this.executor.weightMap; // Attach a model-level resourceManager to the initializer, the // hashTables created from when executing the initializer will be stored // in the resourceManager. this.initializer.resourceManager = this.resourceManager; this.initializerSignature = artifacts.initializerSignature; } return true; } /** * Save the configuration and/or weights of the GraphModel. * * An `IOHandler` is an object that has a `save` method of the proper * signature defined. The `save` method manages the storing or * transmission of serialized data ("artifacts") that represent the * model's topology and weights onto or via a specific medium, such as * file downloads, local storage, IndexedDB in the web browser and HTTP * requests to a server. TensorFlow.js provides `IOHandler` * implementations for a number of frequently used saving mediums, such as * `tf.io.browserDownloads` and `tf.io.browserLocalStorage`. See `tf.io` * for more details. * * This method also allows you to refer to certain types of `IOHandler`s * as URL-like string shortcuts, such as 'localstorage://' and * 'indexeddb://'. * * Example 1: Save `model`'s topology and weights to browser [local * storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage); * then load it back. * * ```js * const modelUrl = * 'https://storage.googleapis.com/tfjs-models/savedmodel/mobilenet_v2_1.0_224/model.json'; * const model = await tf.loadGraphModel(modelUrl); * const zeros = tf.zeros([1, 224, 224, 3]); * model.predict(zeros).print(); * * const saveResults = await model.save('localstorage://my-model-1'); * * const loadedModel = await tf.loadGraphModel('localstorage://my-model-1'); * console.log('Prediction from loaded model:'); * model.predict(zeros).print(); * ``` * * @param handlerOrURL An instance of `IOHandler` or a URL-like, * scheme-based string shortcut for `IOHandler`. * @param config Options for saving the model. * @returns A `Promise` of `SaveResult`, which summarizes the result of * the saving, such as byte sizes of the saved artifacts for the model's * topology and weight values. * * @doc {heading: 'Models', subheading: 'Classes', ignoreCI: true} */ async save(handlerOrURL, config) { if (typeof handlerOrURL === 'string') { const handlers = this.io.getSaveHandlers(handlerOrURL); if (handlers.length === 0) { throw new Error(`Cannot find any save handlers for URL '${handlerOrURL}'`); } else if (handlers.length > 1) { throw new Error(`Found more than one (${handlers.length}) save handlers for ` + `URL '${handlerOrURL}'`); } handlerOrURL = handlers[0]; } if (handlerOrURL.save == null) { throw new Error('GraphModel.save() cannot proceed because the IOHandler ' + 'provided does not have the `save` attribute defined.'); } return handlerOrURL.save(this.artifacts); } addStructuredOutputNames(outputTensors) { if (this.structuredOutputKeys) { const outputTensorsArray = outputTensors instanceof Tensor ? [outputTensors] : outputTensors; const outputTensorMap = {}; outputTensorsArray.forEach((outputTensor, i) => outputTensorMap[this.structuredOutputKeys[i]] = outputTensor); return outputTensorMap; } return outputTensors; } /** * Execute the inference for the input tensors. * * @param input The input tensors, when there is single input for the model, * inputs param should be a `tf.Tensor`. For models with multiple inputs, * inputs params should be in either `tf.Tensor`[] if the input order is * fixed, or otherwise NamedTensorMap format. * * For model with multiple inputs, we recommend you use NamedTensorMap as the * input type, if you use `tf.Tensor`[], the order of the array needs to * follow the * order of inputNodes array. @see {@link GraphModel.inputNodes} * * You can also feed any intermediate nodes using the NamedTensorMap as the * input type. For example, given the graph * InputNode => Intermediate => OutputNode, * you can execute the subgraph Intermediate => OutputNode by calling * model.execute('IntermediateNode' : tf.tensor(...)); * * This is useful for models that uses tf.dynamic_rnn, where the intermediate * state needs to be fed manually. * * For batch inference execution, the tensors for each input need to be * concatenated together. For example with mobilenet, the required input shape * is [1, 244, 244, 3], which represents the [batch, height, width, channel]. * If we are provide a batched data of 100 images, the input tensor should be * in the shape of [100, 244, 244, 3]. * * @param config Prediction configuration for specifying the batch size. * Currently the batch size option is ignored for graph model. * * @returns Inference result tensors. If the model is converted and it * originally had structured_outputs in tensorflow, then a NamedTensorMap * will be returned matching the structured_outputs. If no structured_outputs * are present, the output will be single `tf.Tensor` if the model has single * output node, otherwise Tensor[]. * * @doc {heading: 'Models', subheading: 'Classes'} */ predict(inputs, config) { const outputTensors = this.execute(inputs, this.outputNodes); return this.addStructuredOutputNames(outputTensors); } /** * Execute the inference for the input tensors in async fashion, use this * method when your model contains control flow ops. * * @param input The input tensors, when there is single input for the model, * inputs param should be a `tf.Tensor`. For models with mutliple inputs, * inputs params should be in either `tf.Tensor`[] if the input order is * fixed, or otherwise NamedTensorMap format. * * For model with multiple inputs, we recommend you use NamedTensorMap as the * input type, if you use `tf.Tensor`[], the order of the array needs to * follow the * order of inputNodes array. @see {@link GraphModel.inputNodes} * * You can also feed any intermediate nodes using the NamedTensorMap as the * input type. For example, given the graph * InputNode => Intermediate => OutputNode, * you can execute the subgraph Intermediate => OutputNode by calling * model.execute('IntermediateNode' : tf.tensor(...)); * * This is useful for models that uses tf.dynamic_rnn, where the intermediate * state needs to be fed manually. * * For batch inference execution, the tensors for each input need to be * concatenated together. For example with mobilenet, the required input shape * is [1, 244, 244, 3], which represents the [batch, height, width, channel]. * If we are provide a batched data of 100 images, the input tensor should be * in the shape of [100, 244, 244, 3]. * * @param config Prediction configuration for specifying the batch size. * Currently the batch size option is ignored for graph model. * * @returns A Promise of inference result tensors. If the model is converted * and it originally had structured_outputs in tensorflow, then a * NamedTensorMap will be returned matching the structured_outputs. If no * structured_outputs are present, the output will be single `tf.Tensor` if * the model has single output node, otherwise Tensor[]. * * @doc {heading: 'Models', subheading: 'Classes'} */ async predictAsync(inputs, config) { const outputTensors = await this.executeAsync(inputs, this.outputNodes); return this.addStructuredOutputNames(outputTensors); } normalizeInputs(inputs) { var _a; if (!(inputs instanceof Tensor) && !Array.isArray(inputs)) { // The input is already a NamedTensorMap. const signatureInputs = (_a = this.signature) === null || _a === void 0 ? void 0 : _a.inputs; if (signatureInputs != null) { for (const input in signatureInputs) { const tensor = signatureInputs[input]; if (tensor.resourceId != null) { inputs[input] = this.resourceIdToCapturedInput[tensor.resourceId]; } } } return inputs; } inputs = Array.isArray(inputs) ? inputs : [inputs]; const numCapturedInputs = Object.keys(this.resourceIdToCapturedInput).length; if (inputs.length + numCapturedInputs !== this.inputNodes.length) { throw new Error(`Input tensor count mismatch, the graph model has ${this.inputNodes.length - numCapturedInputs} non-resource placeholders, while there are ${inputs.length} input tensors provided.`); } let inputIndex = 0; return this.inputNodes.reduce((map, inputName) => { var _a, _b, _c; const resourceId = (_c = (_b = (_a = this.signature) === null || _a === void 0 ? void 0 : _a.inputs) === null || _b === void 0 ? void 0 : _b[inputName]) === null || _c === void 0 ? void 0 : _c.resourceId; if (resourceId != null) { map[inputName] = this.resourceIdToCapturedInput[resourceId]; } else { map[inputName] = inputs[inputIndex++]; } return map; }, {}); } normalizeOutputs(outputs) { outputs = outputs || this.outputNodes; return !Array.isArray(outputs) ? [outputs] : outputs; } executeInitializerGraph() { if (this.initializer == null) { return []; } if (this.initializerSignature == null) { return this.initializer.execute({}, []); } else { return this.initializer.execute({}, Object.keys(this.initializerSignature.outputs)); } } async executeInitializerGraphAsync() { if (this.initializer == null) { return []; } if (this.initializerSignature == null) { return this.initializer.executeAsync({}, []); } else { return this.initializer.executeAsync({}, Object.keys(this.initializerSignature.outputs)); } } setResourceIdToCapturedInput(outputs) { this.resourceIdToCapturedInput = {}; if (this.initializerSignature) { const signatureOutputs = this.initializerSignature.outputs; const outputNames = Object.keys(signatureOutputs); for (let i = 0; i < outputNames.length; i++) { const outputName = outputNames[i]; const tensorInfo = signatureOutputs[outputName]; this.resourceIdToCapturedInput[tensorInfo.resourceId] = outputs[i]; } } } /** * Executes inference for the model for given input tensors. * @param inputs tensor, tensor array or tensor map of the inputs for the * model, keyed by the input node names. * @param outputs output node name from the TensorFlow model, if no * outputs are specified, the default outputs of the model would be used. * You can inspect intermediate nodes of the model by adding them to the * outputs array. * * @returns A single tensor if provided with a single output or no outputs * are provided and there is only one default output, otherwise return a * tensor array. The order of the tensor array is the same as the outputs * if provided, otherwise the order of outputNodes attribute of the model. * * @doc {heading: 'Models', subheading: 'Classes'} */ execute(inputs, outputs) { if (this.resourceIdToCapturedInput == null) { this.setResourceIdToCapturedInput(this.executeInitializerGraph()); } inputs = this.normalizeInputs(inputs); outputs = this.normalizeOutputs(outputs); const result = this.executor.execute(inputs, outputs); return result.length > 1 ? result : result[0]; } /** * Executes inference for the model for given input tensors in async * fashion, use this method when your model contains control flow ops. * @param inputs tensor, tensor array or tensor map of the inputs for the * model, keyed by the input node names. * @param outputs output node name from the TensorFlow model, if no outputs * are specified, the default outputs of the model would be used. You can * inspect intermediate nodes of the model by adding them to the outputs * array. * * @returns A Promise of single tensor if provided with a single output or * no outputs are provided and there is only one default output, otherwise * return a tensor map. * * @doc {heading: 'Models', subheading: 'Classes'} */ async executeAsync(inputs, outputs) { if (this.resourceIdToCapturedInput == null) { this.setResourceIdToCapturedInput(await this.executeInitializerGraphAsync()); } inputs = this.normalizeInputs(inputs); outputs = this.normalizeOutputs(outputs); const result = await this.executor.executeAsync(inputs, outputs); return result.length > 1 ? result : result[0]; } /** * Get intermediate tensors for model debugging mode (flag * KEEP_INTERMEDIATE_TENSORS is true). * * @doc {heading: 'Models', subheading: 'Classes'} */ getIntermediateTensors() { return this.executor.getIntermediateTensors(); } /** * Dispose intermediate tensors for model debugging mode (flag * KEEP_INTERMEDIATE_TENSORS is true). * * @doc {heading: 'Models', subheading: 'Classes'} */ disposeIntermediateTensors() { this.executor.disposeIntermediateTensors(); } convertTensorMapToTensorsMap(map) { return Object.keys(map).reduce((newMap, key) => { newMap[key] = [map[key]]; return newMap; }, {}); } /** * Releases the memory used by the weight tensors and resourceManager. * * @doc {heading: 'Models', subheading: 'Classes'} */ dispose() { this.executor.dispose(); if (this.initializer) { this.initializer.dispose(); if (this.resourceIdToCapturedInput) { dispose(this.resourceIdToCapturedInput); } } this.resourceManager.dispose(); } } /** * Load a graph model given a URL to the model definition. * * Example of loading MobileNetV2 from a URL and making a prediction with a * zeros input: * * ```js * const modelUrl = * 'https://storage.googleapis.com/tfjs-models/savedmodel/mobilenet_v2_1.0_224/model.json'; * const model = await tf.loadGraphModel(modelUrl); * const zeros = tf.zeros([1, 224, 224, 3]); * model.predict(zeros).print(); * ``` * * Example of loading MobileNetV2 from a TF Hub URL and making a prediction * with a zeros input: * * ```js * const modelUrl = * 'https://tfhub.dev/google/imagenet/mobilenet_v2_140_224/classification/2'; * const model = await tf.loadGraphModel(modelUrl, {fromTFHub: true}); * const zeros = tf.zeros([1, 224, 224, 3]); * model.predict(zeros).print(); * ``` * @param modelUrl The url or an `io.IOHandler` that loads the model. * @param options Options for the HTTP request, which allows to send * credentials * and custom headers. * * @doc {heading: 'Models', subheading: 'Loading'} */ export async function loadGraphModel(modelUrl, options = {}, tfio = io) { if (modelUrl == null) { throw new Error('modelUrl in loadGraphModel() cannot be null. Please provide a url ' + 'or an IOHandler that loads the model'); } if (options == null) { options = {}; } if (options.fromTFHub && typeof modelUrl === 'string') { modelUrl = getTFHubUrl(modelUrl); } const model = new GraphModel(modelUrl, options, tfio); await model.load(); return model; } /** * Load a graph model given a synchronous IO handler with a 'load' method. * * @param modelSource The `io.IOHandlerSync` that loads the model, or the * `io.ModelArtifacts` that encode the model, or a tuple of * `[io.ModelJSON, ArrayBuffer]` of which the first element encodes the * model and the second contains the weights. * * @doc {heading: 'Models', subheading: 'Loading'} */ export function loadGraphModelSync(modelSource) { if (modelSource == null) { throw new Error('modelUrl in loadGraphModelSync() cannot be null. Please provide ' + 'model artifacts or an IOHandler that loads the model'); } let ioHandler; if (modelSource instanceof Array) { const [modelJSON, weights] = modelSource; if (!modelJSON) { throw new Error('modelJSON must be the first element of the array'); } if (!weights || !(weights instanceof ArrayBuffer)) { throw new Error('An ArrayBuffer of weights must be the second element of' + ' the array'); } if (!('modelTopology' in modelJSON)) { throw new Error('Model JSON is missing \'modelTopology\''); } if (!('weightsManifest' in modelJSON)) { throw new Error('Model JSON is missing \'weightsManifest\''); } const weightSpecs = io.getWeightSpecs(modelJSON.weightsManifest); const modelArtifacts = io.getModelArtifactsForJSONSync(modelJSON, weightSpecs, weights); ioHandler = io.fromMemorySync(modelArtifacts); } else if ('load' in modelSource) { // Then modelSource is already an IOHandlerSync. ioHandler = modelSource; } else if ('modelTopology' in modelSource && 'weightSpecs' in modelSource && 'weightData' in modelSource) { // modelSource is of type ModelArtifacts. ioHandler = io.fromMemorySync(modelSource); } else { throw new Error('Unknown model format'); } const model = new GraphModel(ioHandler); model.load(); return model; } function getTFHubUrl(modelUrl) { if (!modelUrl.endsWith('/')) { modelUrl = (modelUrl) + '/'; } return `${modelUrl}${DEFAULT_MODEL_NAME}${TFHUB_SEARCH_PARAM}`; } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ3JhcGhfbW9kZWwuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi90ZmpzLWNvbnZlcnRlci9zcmMvZXhlY3V0b3IvZ3JhcGhfbW9kZWwudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7Ozs7OztHQWVHO0FBRUgsT0FBTyxFQUFDLE9BQU8sRUFBa0IsRUFBRSxFQUFzQyxNQUFNLEVBQUUsSUFBSSxFQUFDLE1BQU0sdUJBQXVCLENBQUM7QUFJcEgsT0FBTyxFQUFDLGVBQWUsRUFBQyxNQUFNLGdDQUFnQyxDQUFDO0FBRS9ELE9BQU8sRUFBQyxhQUFhLEVBQUMsTUFBTSxrQkFBa0IsQ0FBQztBQUMvQyxPQUFPLEVBQUMsZUFBZSxFQUFDLE1BQU0sb0JBQW9CLENBQUM7QUFDbkQsaURBQWlEO0FBQ2pELE9BQU8sRUFBQyxtQkFBbUIsRUFBQyxNQUFNLHdDQUF3QyxDQUFDO0FBRTNFLE1BQU0sQ0FBQyxNQUFNLGtCQUFrQixHQUFHLG1CQUFtQixDQUFDO0FBQ3RELE1BQU0sQ0FBQyxNQUFNLGtCQUFrQixHQUFHLFlBQVksQ0FBQztBQUkvQzs7Ozs7Ozs7O0dBU0c7QUFDSCxNQUFNLE9BQU8sVUFBVTtJQWNyQixxRUFBcUU7SUFDckUsSUFBSSxZQUFZO1FBQ2QsT0FBTyxJQUFJLENBQUMsT0FBTyxDQUFDO0lBQ3RCLENBQUM7SUFFRCxJQUFJLFVBQVU7UUFDWixPQUFPLElBQUksQ0FBQyxRQUFRLENBQUMsVUFBVSxDQUFDO0lBQ2xDLENBQUM7SUFFRCxJQUFJLFdBQVc7UUFDYixPQUFPLElBQUksQ0FBQyxRQUFRLENBQUMsV0FBVyxDQUFDO0lBQ25DLENBQUM7SUFFRCxJQUFJLE1BQU07UUFDUixPQUFPLElBQUksQ0FBQyxRQUFRLENBQUMsTUFBTSxDQUFDO0lBQzlCLENBQUM7SUFFRCxJQUFJLE9BQU87UUFDVCxPQUFPLElBQUksQ0FBQyxRQUFRLENBQUMsT0FBTyxDQUFDO0lBQy9CLENBQUM7SUFF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QUFJLGFBQWEsSUFBSSxXQUFXO1FBQzlELFlBQVksSUFBSSxXQUFXLEVBQUU7UUFDL0IseUNBQXlDO1FBQ3pDLFNBQVMsR0FBRyxFQUFFLENBQUMsY0FBYyxDQUFDLFdBQVcsQ0FBQyxDQUFDO0tBQzVDO1NBQU07UUFDTCxNQUFNLElBQUksS0FBSyxDQUFDLHNCQUFzQixDQUFDLENBQUM7S0FDekM7SUFFRCxNQUFNLEtBQUssR0FBRyxJQUFJLFVBQVUsQ0FBQyxTQUFTLENBQUMsQ0FBQztJQUN4QyxLQUFLLENBQUMsSUFBSSxFQUFFLENBQUM7SUFDYixPQUFPLEtBQUssQ0FBQztBQUNmLENBQUM7QUFFRCxTQUFTLFdBQVcsQ0FBQyxRQUFnQjtJQUNuQyxJQUFJLENBQUMsUUFBUSxDQUFDLFFBQVEsQ0FBQyxHQUFHLENBQUMsRUFBRTtRQUMzQixRQUFRLEdBQUcsQ0FBQyxRQUFRLENBQUMsR0FBRyxHQUFHLENBQUM7S0FDN0I7SUFDRCxPQUFPLEdBQUcsUUFBUSxHQUFHLGtCQUFrQixHQUFHLGtCQUFrQixFQUFFLENBQUM7QUFDakUsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogQGxpY2Vuc2VcbiAqIENvcHlyaWdodCAyMDE4IEdvb2dsZSBMTEMuIEFsbCBSaWdodHMgUmVzZXJ2ZWQuXG4gKiBMaWNlbnNlZCB1bmRlciB0aGUgQXBhY2hlIExpY2Vuc2UsIFZlcnNpb24gMi4wICh0aGUgXCJMaWNlbnNlXCIpO1xuICogeW91IG1heSBub3QgdXNlIHRoaXMgZmlsZSBleGNlcHQgaW4gY29tcGxpYW5jZSB3aXRoIHRoZSBMaWNlbnNlLlxuICogWW91IG1heSBvYnRhaW4gYSBjb3B5IG9mIHRoZSBMaWNlbnNlIGF0XG4gKlxuICogaHR0cDovL3d3dy5hcGFjaGUub3JnL2xpY2Vuc2VzL0xJQ0VOU0UtMi4wXG4gKlxuICogVW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzb2Z0d2FyZVxuICogZGlzdHJpYnV0ZWQgdW5kZXIgdGhlIExpY2Vuc2UgaXMgZGlzdHJpYnV0ZWQgb24gYW4gXCJBUyBJU1wiIEJBU0lTLFxuICogV0lUSE9VVCBXQVJSQU5USUVTIE9SIENPTkRJVElPTlMgT0YgQU5ZIEtJTkQsIGVpdGhlciBleHByZXNzIG9yIGltcGxpZWQuXG4gKiBTZWUgdGhlIExpY2Vuc2UgZm9yIHRoZSBzcGVjaWZpYyBsYW5ndWFnZSBnb3Zlcm5pbmcgcGVybWlzc2lvbnMgYW5kXG4gKiBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS5cbiAqID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09XG4gKi9cblxuaW1wb3J0IHtkaXNwb3NlLCBJbmZlcmVuY2VNb2RlbCwgaW8sIE1vZGVsUHJlZGljdENvbmZpZywgTmFtZWRUZW5zb3JNYXAsIFRlbnNvciwgdXRpbH0gZnJvbSAnQHRlbnNvcmZsb3cvdGZqcy1jb3JlJztcblxuaW1wb3J0ICogYXMgdGVuc29yZmxvdyBmcm9tICcuLi9kYXRhL2NvbXBpbGVkX2FwaSc7XG5pbXBvcnQge05hbWVkVGVuc29yc01hcCwgVGVuc29ySW5mb30gZnJvbSAnLi4vZGF0YS90eXBlcyc7XG5pbXBvcnQge09wZXJhdGlvbk1hcHBlcn0gZnJvbSAnLi4vb3BlcmF0aW9ucy9vcGVyYXRpb25fbWFwcGVyJztcblxuaW1wb3J0IHtHcmFwaEV4ZWN1dG9yfSBmcm9tICcuL2dyYXBoX2V4ZWN1dG9yJztcbmltcG9ydCB7UmVzb3VyY2VNYW5hZ2VyfSBmcm9tICcuL3Jlc291cmNlX21hbmFnZXInO1xuLy8gdHNsaW50OmRpc2FibGUtbmV4dC1saW5lOiBuby1pbXBvcnRzLWZyb20tZGlzdFxuaW1wb3J0IHtkZWNvZGVXZWlnaHRzU3RyZWFtfSBmcm9tICdAdGVuc29yZmxvdy90ZmpzLWNvcmUvZGlzdC9pby9pb191dGlscyc7XG5cbmV4cG9ydCBjb25zdCBURkhVQl9TRUFSQ0hfUEFSQU0gPSAnP3RmanMtZm9ybWF0PWZpbGUnO1xuZXhwb3J0IGNvbnN0IERFRkFVTFRfTU9ERUxfTkFNRSA9ICdtb2RlbC5qc29uJztcbnR5cGUgVXJsID0gc3RyaW5nfGlvLklPSGFuZGxlcnxpby5JT0hhbmRsZXJTeW5jO1xudHlwZSBVcmxJT0hhbmRsZXI8VCBleHRlbmRzIFVybD4gPSBUIGV4dGVuZHMgc3RyaW5nID8gaW8uSU9IYW5kbGVyIDogVDtcblxuLyoqXG4gKiBBIGB0Zi5HcmFwaE1vZGVsYCBpcyBhIGRpcmVjdGVkLCBhY3ljbGljIGdyYXBoIGJ1aWx0IGZyb20gYVxuICogU2F2ZWRNb2RlbCBHcmFwaERlZiBhbmQgYWxsb3dzIGluZmVyZW5jZSBleGVjdXRpb24uXG4gKlxuICogQSBgdGYuR3JhcGhNb2RlbGAgY2FuIG9ubHkgYmUgY3JlYXRlZCBieSBsb2FkaW5nIGZyb20gYSBtb2RlbCBjb252ZXJ0ZWQgZnJvbVxuICogYSBbVGVuc29yRmxvdyBTYXZlZE1vZGVsXShodHRwczovL3d3dy50ZW5zb3JmbG93Lm9yZy9ndWlkZS9zYXZlZF9tb2RlbCkgdXNpbmdcbiAqIHRoZSBjb21tYW5kIGxpbmUgY29udmVydGVyIHRvb2wgYW5kIGxvYWRlZCB2aWEgYHRmLmxvYWRHcmFwaE1vZGVsYC5cbiAqXG4gKiBAZG9jIHtoZWFkaW5nOiAnTW9kZWxzJywgc3ViaGVhZGluZzogJ0NsYXNzZXMnfVxuICovXG5leHBvcnQgY2xhc3MgR3JhcGhNb2RlbDxNb2RlbFVSTCBleHRlbmRzIFVybCA9IHN0cmluZyB8IGlvLklPSGFuZGxlcj4gaW1wbGVtZW50c1xuICAgIEluZmVyZW5jZU1vZGVsIHtcbiAgcHJpdmF0ZSBleGVjdXRvcjogR3JhcGhFeGVjdXRvcjtcbiAgcHJpdmF0ZSB2ZXJzaW9uID0gJ24vYSc7XG4gIHByaXZhdGUgaGFuZGxlcjogVXJsSU9IYW5kbGVyPE1vZGVsVVJMPjtcbiAgcHJpdmF0ZSBhcnRpZmFjdHM6IGlvLk1vZGVsQXJ0aWZhY3RzO1xuICBwcml2YXRlIGluaXRpYWxpemVyOiBHcmFwaEV4ZWN1dG9yO1xuICBwcml2YXRlIHJlc291cmNlSWRUb0NhcHR1cmVkSW5wdXQ6IHtba2V5OiBudW1iZXJdOiBUZW5zb3J9O1xuICBwcml2YXRlIHJlc291cmNlTWFuYWdlcjogUmVzb3VyY2VNYW5hZ2VyO1xuICBwcml2YXRlIHNpZ25hdHVyZTogdGVuc29yZmxvdy5JU2lnbmF0dXJlRGVmO1xuICBwcml2YXRlIGluaXRpYWxpemVyU2lnbmF0dXJlOiB0ZW5zb3JmbG93LklTaWduYXR1cmVEZWY7XG4gIHByaXZhdGUgc3RydWN0dXJlZE91dHB1dEtleXM6IHN0cmluZ1tdO1xuICBwcml2YXRlIHJlYWRvbmx5IGlvOiB0eXBlb2YgaW87XG5cbiAgLy8gUmV0dXJucyB0aGUgdmVyc2lvbiBpbmZvcm1hdGlvbiBmb3IgdGhlIHRlbnNvcmZsb3cgbW9kZWwgR3JhcGhEZWYuXG4gIGdldCBtb2RlbFZlcnNpb24oKTogc3RyaW5nIHtcbiAgICByZXR1cm4gdGhpcy52ZXJzaW9uO1xuICB9XG5cbiAgZ2V0IGlucHV0Tm9kZXMoKTogc3RyaW5nW10ge1xuICAgIHJldHVybiB0aGlzLmV4ZWN1dG9yLmlucHV0Tm9kZXM7XG4gIH1cblxuICBnZXQgb3V0cHV0Tm9kZXMoKTogc3RyaW5nW10ge1xuICAgIHJldHVybiB0aGlzLmV4ZWN1dG9yLm91dHB1dE5vZGVzO1xuICB9XG5cbiAgZ2V0IGlucHV0cygpOiBUZW5zb3JJbmZvW10ge1xuICAgIHJldHVybiB0aGlzLmV4ZWN1dG9yLmlucHV0cztcbiAgfVxuXG4gIGdldCBvdXRwdXRzKCk6IFRlbnNvckluZm9bXSB7XG4gICAgcmV0dXJuIHRoaXMuZXhlY3V0b3Iub3V0cHV0cztcbiAgfVxuXG4gIGdldCB3ZWlnaHRzKCk6IE5hbWVkVGVuc29yc01hcCB7XG4gICAgcmV0dXJuIHRoaXMuZXhlY3V0b3Iud2VpZ2h0TWFwO1xuICB9XG5cbiAgZ2V0IG1ldGFkYXRhKCk6IHt9IHtcbiAgICByZXR1cm4gdGhpcy5hcnRpZmFjdHMudXNlckRlZmluZWRNZXRhZGF0YTtcbiAgfVxuXG4gIGdldCBtb2RlbFNpZ25hdHVyZSgpOiB7fSB7XG4gICAgcmV0dXJuIHRoaXMuc2lnbmF0dXJlO1xuICB9XG5cbiAgZ2V0IG1vZGVsU3RydWN0dXJlZE91dHB1dEtleXMoKToge30ge1xuICAgIHJldHVybiB0aGlzLnN0cnVjdHVyZWRPdXRwdXRLZXlzO1xuICB9XG5cbiAgLyoqXG4gICAqIEBwYXJhbSBtb2RlbFVybCB1cmwgZm9yIHRoZSBtb2RlbCwgb3IgYW4gYGlvLklPSGFuZGxlcmAuXG4gICAqIEBwYXJhbSB3ZWlnaHRNYW5pZmVzdFVybCB1cmwgZm9yIHRoZSB3ZWlnaHQgZmlsZSBnZW5lcmF0ZWQgYnlcbiAgICogc2NyaXB0cy9jb252ZXJ0LnB5IHNjcmlwdC5cbiAgICogQHBhcmFtIHJlcXVlc3RPcHRpb24gb3B0aW9ucyBmb3IgUmVxdWVzdCwgd2hpY2ggYWxsb3dzIHRvIHNlbmQgY3JlZGVudGlhbHNcbiAgICogYW5kIGN1c3RvbSBoZWFkZXJzLlxuICAgKiBAcGFyYW0gb25Qcm9ncmVzcyBPcHRpb25hbCwgcHJvZ3Jlc3MgY2FsbGJhY2sgZnVuY3Rpb24sIGZpcmVkIHBlcmlvZGljYWxseVxuICAgKiBiZWZvcmUgdGhlIGxvYWQgaXMgY29tcGxldGVkLlxuICAgKi9cbiAgY29uc3RydWN0b3IoXG4gICAgICBwcml2YXRlIG1vZGVsVXJsOiBNb2RlbFVSTCwgcHJpdmF0ZSBsb2FkT3B0aW9uczogaW8uTG9hZE9wdGlvbnMgPSB7fSxcbiAgICAgIHRmaW8gPSBpbykge1xuICAgIHRoaXMuaW8gPSB0ZmlvO1xuICAgIGlmIChsb2FkT3B0aW9ucyA9PSBudWxsKSB7XG4gICAgICB0aGlzLmxvYWRPcHRpb25zID0ge307XG4gICAgfVxuICAgIHRoaXMucmVzb3VyY2VNYW5hZ2VyID0gbmV3IFJlc291cmNlTWFuYWdlcigpO1xuICB9XG5cbiAgcHJpdmF0ZSBmaW5kSU9IYW5kbGVyKCkge1xuICAgIHR5cGUgSU9IYW5kbGVyID0gVXJsSU9IYW5kbGVyPE1vZGVsVVJMPjtcbiAgICBjb25zdCBwYXRoID0gdGhpcy5tb2RlbFVybDtcbiAgICBpZiAoKHBhdGggYXMgaW8uSU9IYW5kbGVyKS5sb2FkICE9IG51bGwpIHtcbiAgICAgIC8vIFBhdGggaXMgYW4gSU8gSGFuZGxlci5cbiAgICAgIHRoaXMuaGFuZGxlciA9IHBhdGggYXMgSU9IYW5kbGVyO1xuICAgIH0gZWxzZSBpZiAodGhpcy5sb2FkT3B0aW9ucy5yZXF1ZXN0SW5pdCAhPSBudWxsKSB7XG4gICAgICB0aGlzLmhhbmRsZXIgPSB0aGlzLmlvLmJyb3dzZXJIVFRQUmVxdWVzdChcbiAgICAgICAgICAgICAgICAgICAgICAgICBwYXRoIGFzIHN0cmluZywgdGhpcy5sb2FkT3B0aW9ucykgYXMgSU9IYW5kbGVyO1xuICAgIH0gZWxzZSB7XG4gICAgICBjb25zdCBoYW5kbGVycyA9XG4gICAgICAgICAgdGhpcy5pby5nZXRMb2FkSGFuZGxlcnMocGF0aCBhcyBzdHJpbmcsIHRoaXMubG9hZE9wdGlvbnMpO1xuICAgICAgaWYgKGhhbmRsZXJzLmxlbmd0aCA9PT0gMCkge1xuICAgICAgICAvLyBGb3IgYmFja3dhcmQgY29tcGF0aWJpbGl0eTogaWYgbm8gbG9hZCBoYW5kbGVyIGNhbiBiZSBmb3VuZCxcbiAgICAgICAgLy8gYXNzdW1lIGl0IGlzIGEgcmVsYXRpdmUgaHR0cCBwYXRoLlxuICAgICAgICBoYW5kbGVycy5wdXNoKFxuICAgICAgICAgICAgdGhpcy5pby5icm93c2VySFRUUFJlcXVlc3QocGF0aCBhcyBzdHJpbmcsIHRoaXMubG9hZE9wdGlvbnMpKTtcbiAgICAgIH0gZWxzZSBpZiAoaGFuZGxlcnMubGVuZ3RoID4gMSkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoXG4gICAgICAgICAgICBgRm91bmQgbW9yZSB0aGFuIG9uZSAoJHtoYW5kbGVycy5sZW5ndGh9KSBsb2FkIGhhbmRsZXJzIGZvciBgICtcbiAgICAgICAgICAgIGBVUkwgJyR7W3BhdGhdfSdgKTtcbiAgICAgIH1cbiAgICAgIHRoaXMuaGFuZGxlciA9IGhhbmRsZXJzWzBdIGFzIElPSGFuZGxlcjtcbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogTG9hZHMgdGhlIG1vZGVsIGFuZCB3ZWlnaHQgZmlsZXMsIGNvbnN0cnVjdCB0aGUgaW4gbWVtb3J5IHdlaWdodCBtYXAgYW5kXG4gICAqIGNvbXBpbGUgdGhlIGluZmVyZW5jZSBncmFwaC5cbiAgICovXG4gIGxvYWQoKTogVXJsSU9IYW5kbGVyPE1vZGVsVVJMPiBleHRlbmRzIGlvLklPSGFuZGxlclN5bmM/IGJvb2xlYW46XG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBQcm9taXNlPGJvb2xlYW4+IHtcbiAgICB0eXBlIElPSGFuZGxlciA9IFVybElPSGFuZGxlcjxNb2RlbFVSTD47XG4gICAgdGhpcy5maW5kSU9IYW5kbGVyKCk7XG4gICAgaWYgKHRoaXMuaGFuZGxlci5sb2FkID09IG51bGwpIHtcbiAgICAgIHRocm93IG5ldyBFcnJvcihcbiAgICAgICAgICAnQ2Fubm90IHByb2NlZWQgd2l0aCBtb2RlbCBsb2FkaW5nIGJlY2F1c2UgdGhlIElPSGFuZGxlciBwcm92aWRlZCAnICtcbiAgICAgICAgICAnZG9lcyBub3QgaGF2ZSB0aGUgYGxvYWRgIG1ldGhvZCBpbXBsZW1lbnRlZC4nKTtcbiAgICB9XG5cbiAgICB0eXBlIFJlc3VsdCA9XG4gICAgICAgIElPSGFuZGxlciBleHRlbmRzIGlvLklPSGFuZGxlclN5bmMgPyBib29sZWFuIDogUHJvbWlzZTxib29sZWFuPjtcblxuICAgIGNvbnN0IGxvYWRSZXN1bHQgPSB0aGlzLmhhbmRsZXIubG9hZCgpIGFzIFJldHVyblR5cGU8SU9IYW5kbGVyWydsb2FkJ10+O1xuICAgIGlmICh1dGlsLmlzUHJvbWlzZShsb2FkUmVzdWx0KSkge1xuICAgICAgcmV0dXJuIGxvYWRSZXN1bHQudGhlbihhcnRpZmFjdHMgPT4ge1xuICAgICAgICBpZiAoYXJ0aWZhY3RzLmdldFdlaWdodFN0cmVhbSA9PSBudWxsKSB7XG4gICAgICAgICAgcmV0dXJuIHRoaXMubG9hZFN5bmMoYXJ0aWZhY3RzKTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gdGhpcy5sb2FkU3RyZWFtaW5nKGFydGlmYWN0cyk7XG4gICAgICB9KSBhcyBSZXN1bHQ7XG4gICAgfVxuXG4gICAgcmV0dXJuIHRoaXMubG9hZFN5bmMobG9hZFJlc3VsdCkgYXMgUmVzdWx0O1xuICB9XG5cbiAgLyoqXG4gICAqIFN5bmNocm9ub3VzbHkgY29uc3RydWN0IHRoZSBpbi