machine-learning.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. /*! firebase-admin v12.1.1 */
  2. "use strict";
  3. /*!
  4. * Copyright 2020 Google Inc.
  5. *
  6. * Licensed under the Apache License, Version 2.0 (the "License");
  7. * you may not use this file except in compliance with the License.
  8. * You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing, software
  13. * distributed under the License is distributed on an "AS IS" BASIS,
  14. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. * See the License for the specific language governing permissions and
  16. * limitations under the License.
  17. */
  18. Object.defineProperty(exports, "__esModule", { value: true });
  19. exports.Model = exports.MachineLearning = void 0;
  20. const index_1 = require("../storage/index");
  21. const error_1 = require("../utils/error");
  22. const validator = require("../utils/validator");
  23. const deep_copy_1 = require("../utils/deep-copy");
  24. const utils = require("../utils");
  25. const machine_learning_api_client_1 = require("./machine-learning-api-client");
  26. const machine_learning_utils_1 = require("./machine-learning-utils");
  27. /**
  28. * The Firebase `MachineLearning` service interface.
  29. */
  30. class MachineLearning {
  31. /**
  32. * @param app - The app for this ML service.
  33. * @constructor
  34. * @internal
  35. */
  36. constructor(app) {
  37. if (!validator.isNonNullObject(app) || !('options' in app)) {
  38. throw new error_1.FirebaseError({
  39. code: 'machine-learning/invalid-argument',
  40. message: 'First argument passed to admin.machineLearning() must be a ' +
  41. 'valid Firebase app instance.',
  42. });
  43. }
  44. this.appInternal = app;
  45. this.client = new machine_learning_api_client_1.MachineLearningApiClient(app);
  46. }
  47. /**
  48. * The {@link firebase-admin.app#App} associated with the current `MachineLearning`
  49. * service instance.
  50. */
  51. get app() {
  52. return this.appInternal;
  53. }
  54. /**
  55. * Creates a model in the current Firebase project.
  56. *
  57. * @param model - The model to create.
  58. *
  59. * @returns A Promise fulfilled with the created model.
  60. */
  61. createModel(model) {
  62. return this.signUrlIfPresent(model)
  63. .then((modelContent) => this.client.createModel(modelContent))
  64. .then((operation) => this.client.handleOperation(operation))
  65. .then((modelResponse) => new Model(modelResponse, this.client));
  66. }
  67. /**
  68. * Updates a model's metadata or model file.
  69. *
  70. * @param modelId - The ID of the model to update.
  71. * @param model - The model fields to update.
  72. *
  73. * @returns A Promise fulfilled with the updated model.
  74. */
  75. updateModel(modelId, model) {
  76. const updateMask = utils.generateUpdateMask(model);
  77. return this.signUrlIfPresent(model)
  78. .then((modelContent) => this.client.updateModel(modelId, modelContent, updateMask))
  79. .then((operation) => this.client.handleOperation(operation))
  80. .then((modelResponse) => new Model(modelResponse, this.client));
  81. }
  82. /**
  83. * Publishes a Firebase ML model.
  84. *
  85. * A published model can be downloaded to client apps.
  86. *
  87. * @param modelId - The ID of the model to publish.
  88. *
  89. * @returns A Promise fulfilled with the published model.
  90. */
  91. publishModel(modelId) {
  92. return this.setPublishStatus(modelId, true);
  93. }
  94. /**
  95. * Unpublishes a Firebase ML model.
  96. *
  97. * @param modelId - The ID of the model to unpublish.
  98. *
  99. * @returns A Promise fulfilled with the unpublished model.
  100. */
  101. unpublishModel(modelId) {
  102. return this.setPublishStatus(modelId, false);
  103. }
  104. /**
  105. * Gets the model specified by the given ID.
  106. *
  107. * @param modelId - The ID of the model to get.
  108. *
  109. * @returns A Promise fulfilled with the model object.
  110. */
  111. getModel(modelId) {
  112. return this.client.getModel(modelId)
  113. .then((modelResponse) => new Model(modelResponse, this.client));
  114. }
  115. /**
  116. * Lists the current project's models.
  117. *
  118. * @param options - The listing options.
  119. *
  120. * @returns A promise that
  121. * resolves with the current (filtered) list of models and the next page
  122. * token. For the last page, an empty list of models and no page token
  123. * are returned.
  124. */
  125. listModels(options = {}) {
  126. return this.client.listModels(options)
  127. .then((resp) => {
  128. if (!validator.isNonNullObject(resp)) {
  129. throw new machine_learning_utils_1.FirebaseMachineLearningError('invalid-argument', `Invalid ListModels response: ${JSON.stringify(resp)}`);
  130. }
  131. let models = [];
  132. if (resp.models) {
  133. models = resp.models.map((rs) => new Model(rs, this.client));
  134. }
  135. const result = { models };
  136. if (resp.nextPageToken) {
  137. result.pageToken = resp.nextPageToken;
  138. }
  139. return result;
  140. });
  141. }
  142. /**
  143. * Deletes a model from the current project.
  144. *
  145. * @param modelId - The ID of the model to delete.
  146. */
  147. deleteModel(modelId) {
  148. return this.client.deleteModel(modelId);
  149. }
  150. setPublishStatus(modelId, publish) {
  151. const updateMask = ['state.published'];
  152. const options = { state: { published: publish } };
  153. return this.client.updateModel(modelId, options, updateMask)
  154. .then((operation) => this.client.handleOperation(operation))
  155. .then((modelResponse) => new Model(modelResponse, this.client));
  156. }
  157. signUrlIfPresent(options) {
  158. const modelOptions = (0, deep_copy_1.deepCopy)(options);
  159. if ((0, machine_learning_api_client_1.isGcsTfliteModelOptions)(modelOptions)) {
  160. return this.signUrl(modelOptions.tfliteModel.gcsTfliteUri)
  161. .then((uri) => {
  162. modelOptions.tfliteModel.gcsTfliteUri = uri;
  163. return modelOptions;
  164. })
  165. .catch((err) => {
  166. throw new machine_learning_utils_1.FirebaseMachineLearningError('internal-error', `Error during signing upload url: ${err.message}`);
  167. });
  168. }
  169. return Promise.resolve(modelOptions);
  170. }
  171. signUrl(unsignedUrl) {
  172. const MINUTES_IN_MILLIS = 60 * 1000;
  173. const URL_VALID_DURATION = 10 * MINUTES_IN_MILLIS;
  174. const gcsRegex = /^gs:\/\/([a-z0-9_.-]{3,63})\/(.+)$/;
  175. const matches = gcsRegex.exec(unsignedUrl);
  176. if (!matches) {
  177. throw new machine_learning_utils_1.FirebaseMachineLearningError('invalid-argument', `Invalid unsigned url: ${unsignedUrl}`);
  178. }
  179. const bucketName = matches[1];
  180. const blobName = matches[2];
  181. const bucket = (0, index_1.getStorage)(this.app).bucket(bucketName);
  182. const blob = bucket.file(blobName);
  183. return blob.getSignedUrl({
  184. action: 'read',
  185. expires: Date.now() + URL_VALID_DURATION,
  186. }).then((signUrl) => signUrl[0]);
  187. }
  188. }
  189. exports.MachineLearning = MachineLearning;
  190. /**
  191. * A Firebase ML Model output object.
  192. */
  193. class Model {
  194. /**
  195. * @internal
  196. */
  197. constructor(model, client) {
  198. this.model = Model.validateAndClone(model);
  199. this.client = client;
  200. }
  201. /** The ID of the model. */
  202. get modelId() {
  203. return extractModelId(this.model.name);
  204. }
  205. /**
  206. * The model's name. This is the name you use from your app to load the
  207. * model.
  208. */
  209. get displayName() {
  210. return this.model.displayName;
  211. }
  212. /**
  213. * The model's tags, which can be used to group or filter models in list
  214. * operations.
  215. */
  216. get tags() {
  217. return this.model.tags || [];
  218. }
  219. /** The timestamp of the model's creation. */
  220. get createTime() {
  221. return new Date(this.model.createTime).toUTCString();
  222. }
  223. /** The timestamp of the model's most recent update. */
  224. get updateTime() {
  225. return new Date(this.model.updateTime).toUTCString();
  226. }
  227. /** Error message when model validation fails. */
  228. get validationError() {
  229. return this.model.state?.validationError?.message;
  230. }
  231. /** True if the model is published. */
  232. get published() {
  233. return this.model.state?.published || false;
  234. }
  235. /**
  236. * The ETag identifier of the current version of the model. This value
  237. * changes whenever you update any of the model's properties.
  238. */
  239. get etag() {
  240. return this.model.etag;
  241. }
  242. /**
  243. * The hash of the model's `tflite` file. This value changes only when
  244. * you upload a new TensorFlow Lite model.
  245. */
  246. get modelHash() {
  247. return this.model.modelHash;
  248. }
  249. /** Metadata about the model's TensorFlow Lite model file. */
  250. get tfliteModel() {
  251. // Make a copy so people can't directly modify the private this.model object.
  252. return (0, deep_copy_1.deepCopy)(this.model.tfliteModel);
  253. }
  254. /**
  255. * True if the model is locked by a server-side operation. You can't make
  256. * changes to a locked model. See {@link Model.waitForUnlocked}.
  257. */
  258. get locked() {
  259. return (this.model.activeOperations?.length ?? 0) > 0;
  260. }
  261. /**
  262. * Return the model as a JSON object.
  263. */
  264. toJSON() {
  265. // We can't just return this.model because it has extra fields and
  266. // different formats etc. So we build the expected model object.
  267. const jsonModel = {
  268. modelId: this.modelId,
  269. displayName: this.displayName,
  270. tags: this.tags,
  271. createTime: this.createTime,
  272. updateTime: this.updateTime,
  273. published: this.published,
  274. etag: this.etag,
  275. locked: this.locked,
  276. };
  277. // Also add possibly undefined fields if they exist.
  278. if (this.validationError) {
  279. jsonModel['validationError'] = this.validationError;
  280. }
  281. if (this.modelHash) {
  282. jsonModel['modelHash'] = this.modelHash;
  283. }
  284. if (this.tfliteModel) {
  285. jsonModel['tfliteModel'] = this.tfliteModel;
  286. }
  287. return jsonModel;
  288. }
  289. /**
  290. * Wait for the model to be unlocked.
  291. *
  292. * @param maxTimeMillis - The maximum time in milliseconds to wait.
  293. * If not specified, a default maximum of 2 minutes is used.
  294. *
  295. * @returns A promise that resolves when the model is unlocked
  296. * or the maximum wait time has passed.
  297. */
  298. waitForUnlocked(maxTimeMillis) {
  299. if ((this.model.activeOperations?.length ?? 0) > 0) {
  300. // The client will always be defined on Models that have activeOperations
  301. // because models with active operations came back from the server and
  302. // were constructed with a non-empty client.
  303. return this.client.handleOperation(this.model.activeOperations[0], { wait: true, maxTimeMillis })
  304. .then((modelResponse) => {
  305. this.model = Model.validateAndClone(modelResponse);
  306. });
  307. }
  308. return Promise.resolve();
  309. }
  310. static validateAndClone(model) {
  311. if (!validator.isNonNullObject(model) ||
  312. !validator.isNonEmptyString(model.name) ||
  313. !validator.isNonEmptyString(model.createTime) ||
  314. !validator.isNonEmptyString(model.updateTime) ||
  315. !validator.isNonEmptyString(model.displayName) ||
  316. !validator.isNonEmptyString(model.etag)) {
  317. throw new machine_learning_utils_1.FirebaseMachineLearningError('invalid-server-response', `Invalid Model response: ${JSON.stringify(model)}`);
  318. }
  319. const tmpModel = (0, deep_copy_1.deepCopy)(model);
  320. // If tflite Model is specified, it must have a source of {gcsTfliteUri}
  321. if (model.tfliteModel &&
  322. !validator.isNonEmptyString(model.tfliteModel.gcsTfliteUri)) {
  323. // If we have some other source, ignore the whole tfliteModel.
  324. delete tmpModel.tfliteModel;
  325. }
  326. // Remove '@type' field. We don't need it.
  327. if (tmpModel['@type']) {
  328. delete tmpModel['@type'];
  329. }
  330. return tmpModel;
  331. }
  332. }
  333. exports.Model = Model;
  334. function extractModelId(resourceName) {
  335. return resourceName.split('/').pop();
  336. }