Skip to content

Added CreateModel functionality and tests #788

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5235,9 +5235,7 @@ declare namespace admin.machineLearning {
displayName?: string;
tags?: string[];

tfLiteModel?: {gcsTFLiteUri: string;};

toJSON(forUpload?: boolean): object;
tfliteModel?: {gcsTfliteUri: string;};
}

/**
Expand All @@ -5247,8 +5245,8 @@ declare namespace admin.machineLearning {
readonly modelId: string;
readonly displayName: string;
readonly tags?: string[];
readonly createTime: number;
readonly updateTime: number;
readonly createTime: string;
readonly updateTime: string;
readonly validationError?: string;
readonly published: boolean;
readonly etag: string;
Expand Down
25 changes: 25 additions & 0 deletions src/machine-learning/machine-learning-api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ export interface ModelResponse extends ModelContent {
readonly modelHash?: string;
}

export interface OperationResponse {
readonly name?: string;
readonly done: boolean;
readonly error?: StatusErrorResponse;
readonly response?: ModelResponse;
}


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

public createModel(model: ModelContent): Promise<OperationResponse> {
if (!validator.isNonNullObject(model) ||
!validator.isNonEmptyString(model.displayName)) {
const err = new FirebaseMachineLearningError('invalid-argument', 'Invalid model content.');
return Promise.reject(err);
}
return this.getUrl()
.then((url) => {
const request: HttpRequestConfig = {
method: 'POST',
url: `${url}/models`,
data: model,
};
return this.sendRequest<OperationResponse>(request);
});
}


public getModel(modelId: string): Promise<ModelResponse> {
return Promise.resolve()
.then(() => {
Expand Down
32 changes: 31 additions & 1 deletion src/machine-learning/machine-learning-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,39 @@ export type MachineLearningErrorCode =
| 'not-found'
| 'resource-exhausted'
| 'service-unavailable'
| 'unknown-error';
| 'unknown-error'
| 'cancelled'
| 'deadline-exceeded'
| 'permission-denied'
| 'failed-precondition'
| 'aborted'
| 'out-of-range'
| 'data-loss'
| 'unauthenticated';

export class FirebaseMachineLearningError extends PrefixedFirebaseError {
public static fromOperationError(code: number, message: string): FirebaseMachineLearningError {
switch (code) {
case 1: return new FirebaseMachineLearningError('cancelled', message);
case 2: return new FirebaseMachineLearningError('unknown-error', message);
case 3: return new FirebaseMachineLearningError('invalid-argument', message);
case 4: return new FirebaseMachineLearningError('deadline-exceeded', message);
case 5: return new FirebaseMachineLearningError('not-found', message);
case 6: return new FirebaseMachineLearningError('already-exists', message);
case 7: return new FirebaseMachineLearningError('permission-denied', message);
case 8: return new FirebaseMachineLearningError('resource-exhausted', message);
case 9: return new FirebaseMachineLearningError('failed-precondition', message);
case 10: return new FirebaseMachineLearningError('aborted', message);
case 11: return new FirebaseMachineLearningError('out-of-range', message);
case 13: return new FirebaseMachineLearningError('internal-error', message);
case 14: return new FirebaseMachineLearningError('service-unavailable', message);
case 15: return new FirebaseMachineLearningError('data-loss', message);
case 16: return new FirebaseMachineLearningError('unauthenticated', message);
default:
return new FirebaseMachineLearningError('unknown-error', message);
}
}

constructor(code: MachineLearningErrorCode, message: string) {
super('machine-learning', code, message);
}
Expand Down
81 changes: 69 additions & 12 deletions src/machine-learning/machine-learning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,14 @@
* limitations under the License.
*/


import {FirebaseApp} from '../firebase-app';
import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service';
import {MachineLearningApiClient, ModelResponse} from './machine-learning-api-client';
import {MachineLearningApiClient, ModelResponse, OperationResponse, ModelContent} from './machine-learning-api-client';
import {FirebaseError} from '../utils/error';

import * as validator from '../utils/validator';
import {FirebaseMachineLearningError} from './machine-learning-utils';

// const ML_HOST = 'mlkit.googleapis.com';
import { deepCopy } from '../utils/deep-copy';

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

/**
Expand Down Expand Up @@ -170,10 +170,53 @@ export class MachineLearning implements FirebaseServiceInterface {
public deleteModel(modelId: string): Promise<void> {
return this.client.deleteModel(modelId);
}

private convertOptionstoContent(options: ModelOptions, forUpload?: boolean): Promise<ModelContent> {
const modelContent = deepCopy(options);

if (forUpload && modelContent.tfliteModel?.gcsTfliteUri) {
return this.signUrl(modelContent.tfliteModel.gcsTfliteUri)
.then ((uri: string) => {
modelContent.tfliteModel!.gcsTfliteUri = uri;
return modelContent;
})
.catch((err: Error) => {
throw new FirebaseMachineLearningError(
'internal-error',
`Error during signing upload url: ${err.message}`);
}) as Promise<ModelContent>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is kind of weird. Just define your constant to be ModelContent.

const modelContent = deepCopy(options) as ModelContent;

Copy link
Contributor Author

@ifielker ifielker Feb 20, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I do that I wouldn't be able to assign to it here: modelContent.tfliteModel!.gcsTfliteUri = uri;
because all the ModelContent properties are read only.
(That's why I initially left it as Promise<object>)

}

return Promise.resolve(modelContent) as Promise<ModelContent>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you type modelContent you won't need the cast here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above. I need to cast at the end after all the assignments are complete.

}

private signUrl(unsignedUrl: string): Promise<string> {
const MINUTES_IN_MILLIS = 60 * 1000;
const URL_VALID_DURATION = 10 * MINUTES_IN_MILLIS;

const gcsRegex = /^gs:\/\/([a-z0-9_.-]{3,63})\/(.+)$/;
const matches = gcsRegex.exec(unsignedUrl);
if (!matches) {
throw new FirebaseMachineLearningError(
'invalid-argument',
`Invalid unsigned url: ${unsignedUrl}`);
}
const bucketName = matches[1];
const blobName = matches[2];
const bucket = this.appInternal.storage().bucket(bucketName);
const blob = bucket.file(blobName);
return blob.getSignedUrl({
action: 'read',
expires: Date.now() + URL_VALID_DURATION,
}).then((x) => {
return x[0];
});
}
}


/**
* A Firebase ML Model output object
* A Firebase ML Model output object.
*/
export class Model {
public readonly modelId: string;
Expand All @@ -196,7 +239,7 @@ export class Model {
!validator.isNonEmptyString(model.displayName) ||
!validator.isNonEmptyString(model.etag)) {
throw new FirebaseMachineLearningError(
'invalid-argument',
'invalid-server-response',
`Invalid Model response: ${JSON.stringify(model)}`);
}

Expand Down Expand Up @@ -252,13 +295,27 @@ export class ModelOptions {
public displayName?: string;
public tags?: string[];

public tfliteModel?: { gcsTFLiteUri: string; };

protected toJSON(forUpload?: boolean): object {
throw new Error('NotImplemented');
}
public tfliteModel?: { gcsTfliteUri: string; };
}


function extractModelId(resourceName: string): string {
return resourceName.split('/').pop()!;
}


function handleOperation(op: OperationResponse): Model {
// Backend currently does not return operations that are not done.
if (op.done) {
// Done operations must have either a response or an error.
if (op.response) {
return new Model(op.response);
} else if (op.error) {
throw FirebaseMachineLearningError.fromOperationError(
op.error.code, op.error.message);
}
}
throw new FirebaseMachineLearningError(
'invalid-server-response',
`Invalid Operation response: ${JSON.stringify(op)}`);
}
Loading