1414 * limitations under the License.
1515 */
1616
17-
1817import { FirebaseApp } from '../firebase-app' ;
1918import { 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' ;
2120import { FirebaseError } from '../utils/error' ;
2221
2322import * as validator from '../utils/validator' ;
2423import { 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 = / ^ g s : \/ \/ ( [ a - z 0 - 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 */
178221export 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+
262302function 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