Skip to content

Commit 27f4fb2

Browse files
authored
Added CreateModel functionality and tests (#788)
* Added CreateModel functionality and tests
1 parent 3db0ebe commit 27f4fb2

9 files changed

Lines changed: 599 additions & 32 deletions

File tree

src/index.d.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5235,9 +5235,7 @@ declare namespace admin.machineLearning {
52355235
displayName?: string;
52365236
tags?: string[];
52375237

5238-
tfLiteModel?: {gcsTFLiteUri: string;};
5239-
5240-
toJSON(forUpload?: boolean): object;
5238+
tfliteModel?: {gcsTfliteUri: string;};
52415239
}
52425240

52435241
/**
@@ -5247,8 +5245,8 @@ declare namespace admin.machineLearning {
52475245
readonly modelId: string;
52485246
readonly displayName: string;
52495247
readonly tags?: string[];
5250-
readonly createTime: number;
5251-
readonly updateTime: number;
5248+
readonly createTime: string;
5249+
readonly updateTime: string;
52525250
readonly validationError?: string;
52535251
readonly published: boolean;
52545252
readonly etag: string;

src/machine-learning/machine-learning-api-client.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ export interface ModelResponse extends ModelContent {
5252
readonly modelHash?: string;
5353
}
5454

55+
export interface OperationResponse {
56+
readonly name?: string;
57+
readonly done: boolean;
58+
readonly error?: StatusErrorResponse;
59+
readonly response?: ModelResponse;
60+
}
61+
5562

5663
/**
5764
* Class that facilitates sending requests to the Firebase ML backend API.
@@ -73,6 +80,24 @@ export class MachineLearningApiClient {
7380
this.httpClient = new AuthorizedHttpClient(app);
7481
}
7582

83+
public createModel(model: ModelContent): Promise<OperationResponse> {
84+
if (!validator.isNonNullObject(model) ||
85+
!validator.isNonEmptyString(model.displayName)) {
86+
const err = new FirebaseMachineLearningError('invalid-argument', 'Invalid model content.');
87+
return Promise.reject(err);
88+
}
89+
return this.getUrl()
90+
.then((url) => {
91+
const request: HttpRequestConfig = {
92+
method: 'POST',
93+
url: `${url}/models`,
94+
data: model,
95+
};
96+
return this.sendRequest<OperationResponse>(request);
97+
});
98+
}
99+
100+
76101
public getModel(modelId: string): Promise<ModelResponse> {
77102
return Promise.resolve()
78103
.then(() => {

src/machine-learning/machine-learning-utils.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,39 @@ export type MachineLearningErrorCode =
2525
| 'not-found'
2626
| 'resource-exhausted'
2727
| 'service-unavailable'
28-
| 'unknown-error';
28+
| 'unknown-error'
29+
| 'cancelled'
30+
| 'deadline-exceeded'
31+
| 'permission-denied'
32+
| 'failed-precondition'
33+
| 'aborted'
34+
| 'out-of-range'
35+
| 'data-loss'
36+
| 'unauthenticated';
2937

3038
export class FirebaseMachineLearningError extends PrefixedFirebaseError {
39+
public static fromOperationError(code: number, message: string): FirebaseMachineLearningError {
40+
switch (code) {
41+
case 1: return new FirebaseMachineLearningError('cancelled', message);
42+
case 2: return new FirebaseMachineLearningError('unknown-error', message);
43+
case 3: return new FirebaseMachineLearningError('invalid-argument', message);
44+
case 4: return new FirebaseMachineLearningError('deadline-exceeded', message);
45+
case 5: return new FirebaseMachineLearningError('not-found', message);
46+
case 6: return new FirebaseMachineLearningError('already-exists', message);
47+
case 7: return new FirebaseMachineLearningError('permission-denied', message);
48+
case 8: return new FirebaseMachineLearningError('resource-exhausted', message);
49+
case 9: return new FirebaseMachineLearningError('failed-precondition', message);
50+
case 10: return new FirebaseMachineLearningError('aborted', message);
51+
case 11: return new FirebaseMachineLearningError('out-of-range', message);
52+
case 13: return new FirebaseMachineLearningError('internal-error', message);
53+
case 14: return new FirebaseMachineLearningError('service-unavailable', message);
54+
case 15: return new FirebaseMachineLearningError('data-loss', message);
55+
case 16: return new FirebaseMachineLearningError('unauthenticated', message);
56+
default:
57+
return new FirebaseMachineLearningError('unknown-error', message);
58+
}
59+
}
60+
3161
constructor(code: MachineLearningErrorCode, message: string) {
3262
super('machine-learning', code, message);
3363
}

src/machine-learning/machine-learning.ts

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,14 @@
1414
* limitations under the License.
1515
*/
1616

17-
1817
import {FirebaseApp} from '../firebase-app';
1918
import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service';
20-
import {MachineLearningApiClient, ModelResponse} from './machine-learning-api-client';
19+
import {MachineLearningApiClient, ModelResponse, OperationResponse, ModelContent} from './machine-learning-api-client';
2120
import {FirebaseError} from '../utils/error';
2221

2322
import * as validator from '../utils/validator';
2423
import {FirebaseMachineLearningError} from './machine-learning-utils';
25-
26-
// const ML_HOST = 'mlkit.googleapis.com';
24+
import { deepCopy } from '../utils/deep-copy';
2725

2826
/**
2927
* Internals of an ML instance.
@@ -97,7 +95,9 @@ export class MachineLearning implements FirebaseServiceInterface {
9795
* @return {Promise<Model>} A Promise fulfilled with the created model.
9896
*/
9997
public createModel(model: ModelOptions): Promise<Model> {
100-
throw new Error('NotImplemented');
98+
return this.convertOptionstoContent(model, true)
99+
.then((modelContent) => this.client.createModel(modelContent))
100+
.then((operation) => handleOperation(operation));
101101
}
102102

103103
/**
@@ -170,10 +170,53 @@ export class MachineLearning implements FirebaseServiceInterface {
170170
public deleteModel(modelId: string): Promise<void> {
171171
return this.client.deleteModel(modelId);
172172
}
173+
174+
private convertOptionstoContent(options: ModelOptions, forUpload?: boolean): Promise<ModelContent> {
175+
const modelContent = deepCopy(options);
176+
177+
if (forUpload && modelContent.tfliteModel?.gcsTfliteUri) {
178+
return this.signUrl(modelContent.tfliteModel.gcsTfliteUri)
179+
.then ((uri: string) => {
180+
modelContent.tfliteModel!.gcsTfliteUri = uri;
181+
return modelContent;
182+
})
183+
.catch((err: Error) => {
184+
throw new FirebaseMachineLearningError(
185+
'internal-error',
186+
`Error during signing upload url: ${err.message}`);
187+
}) as Promise<ModelContent>;
188+
}
189+
190+
return Promise.resolve(modelContent) as Promise<ModelContent>;
191+
}
192+
193+
private signUrl(unsignedUrl: string): Promise<string> {
194+
const MINUTES_IN_MILLIS = 60 * 1000;
195+
const URL_VALID_DURATION = 10 * MINUTES_IN_MILLIS;
196+
197+
const gcsRegex = /^gs:\/\/([a-z0-9_.-]{3,63})\/(.+)$/;
198+
const matches = gcsRegex.exec(unsignedUrl);
199+
if (!matches) {
200+
throw new FirebaseMachineLearningError(
201+
'invalid-argument',
202+
`Invalid unsigned url: ${unsignedUrl}`);
203+
}
204+
const bucketName = matches[1];
205+
const blobName = matches[2];
206+
const bucket = this.appInternal.storage().bucket(bucketName);
207+
const blob = bucket.file(blobName);
208+
return blob.getSignedUrl({
209+
action: 'read',
210+
expires: Date.now() + URL_VALID_DURATION,
211+
}).then((x) => {
212+
return x[0];
213+
});
214+
}
173215
}
174216

217+
175218
/**
176-
* A Firebase ML Model output object
219+
* A Firebase ML Model output object.
177220
*/
178221
export class Model {
179222
public readonly modelId: string;
@@ -196,7 +239,7 @@ export class Model {
196239
!validator.isNonEmptyString(model.displayName) ||
197240
!validator.isNonEmptyString(model.etag)) {
198241
throw new FirebaseMachineLearningError(
199-
'invalid-argument',
242+
'invalid-server-response',
200243
`Invalid Model response: ${JSON.stringify(model)}`);
201244
}
202245

@@ -252,13 +295,27 @@ export class ModelOptions {
252295
public displayName?: string;
253296
public tags?: string[];
254297

255-
public tfliteModel?: { gcsTFLiteUri: string; };
256-
257-
protected toJSON(forUpload?: boolean): object {
258-
throw new Error('NotImplemented');
259-
}
298+
public tfliteModel?: { gcsTfliteUri: string; };
260299
}
261300

301+
262302
function extractModelId(resourceName: string): string {
263303
return resourceName.split('/').pop()!;
264304
}
305+
306+
307+
function handleOperation(op: OperationResponse): Model {
308+
// Backend currently does not return operations that are not done.
309+
if (op.done) {
310+
// Done operations must have either a response or an error.
311+
if (op.response) {
312+
return new Model(op.response);
313+
} else if (op.error) {
314+
throw FirebaseMachineLearningError.fromOperationError(
315+
op.error.code, op.error.message);
316+
}
317+
}
318+
throw new FirebaseMachineLearningError(
319+
'invalid-server-response',
320+
`Invalid Operation response: ${JSON.stringify(op)}`);
321+
}

0 commit comments

Comments
 (0)