├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── initialized-ksonnet-kubeflow.tar.gz ├── tensorflow-model ├── Dockerfile ├── MNIST.py └── README.md └── web-ui ├── Dockerfile ├── README.md ├── flask_server.py ├── mnist_client.py ├── static ├── scripts │ └── material.min.js └── styles │ ├── demo.css │ └── material.deep_purple-pink.min.css └── templates └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | ks-kubeflow/environments/* 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution, 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google.com/conduct/). 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction to Kubeflow on Google Kubernetes Engine 2 | 3 | 4 | ### Depreciation Notice 5 | 6 | The [corresponding codelab](https://codelabs.developers.google.com/codelabs/kubeflow-introduction/index.html#0) has been updated to support newer versions of Kubeflow, and this repository has been deprecated in the process. The code associated with the codelab is now available in the official [Kubeflow examples repository](https://github.com/kubeflow/examples/tree/master/mnist) 7 | 8 | --- 9 | 10 | This codelab explains how to use a Google Kubernetes Engine cluster to deploy an open source machine learning stack 11 | (Kubeflow) and make use of it to train and deploy a model that will then serve traffic. 12 | 13 | --- 14 | This is not an officially supported Google product 15 | -------------------------------------------------------------------------------- /initialized-ksonnet-kubeflow.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/kubeflow-introduction/c871d217edb469e356ac7927994bfd7603dc3e9a/initialized-ksonnet-kubeflow.tar.gz -------------------------------------------------------------------------------- /tensorflow-model/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tensorflow/tensorflow:1.10.0 2 | MAINTAINER "Daniel Sanche" 3 | 4 | # add google cloud storage 5 | RUN pip --no-cache-dir install \ 6 | google-cloud-storage \ 7 | && \ 8 | python -m ipykernel.kernelspec 9 | 10 | # include service account key 11 | RUN mkdir /home/tensorflow 12 | ADD key.json /home/tensorflow 13 | ENV GOOGLE_APPLICATION_CREDENTIALS "/home/tensorflow/key.json" 14 | 15 | # show python logs as they occur 16 | ENV PYTHONUNBUFFERED=0 17 | 18 | # include version and bucket as arguments to docker build 19 | ARG version=1 20 | ENV VERSION=$version 21 | ARG bucket 22 | ENV BUCKET=$bucket 23 | 24 | # run MNIST.py with included arguments 25 | ADD MNIST.py /home/tensorflow 26 | WORKDIR /home/tensorflow 27 | ENTRYPOINT /usr/bin/python /home/tensorflow/MNIST.py --version $VERSION --bucket $BUCKET 28 | -------------------------------------------------------------------------------- /tensorflow-model/MNIST.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright 2018 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | ''' 16 | 17 | 18 | import os 19 | 20 | from google.cloud import storage 21 | import tensorflow as tf 22 | from tensorflow.examples.tutorials.mnist import input_data 23 | 24 | ############################################################### 25 | # SETUP 26 | ############################################################### 27 | 28 | # define input arguments 29 | tf.app.flags.DEFINE_string('version', '1', 30 | 'model version') 31 | tf.app.flags.DEFINE_string('bucket', None, 32 | 'name of the GCS bucket') 33 | tf.app.flags.DEFINE_integer('steps', 2000, 34 | 'number of runs through entire training set') 35 | arg_version = tf.app.flags.FLAGS.version 36 | arg_bucket = tf.app.flags.FLAGS.bucket 37 | arg_steps = tf.app.flags.FLAGS.steps 38 | 39 | # network parameters 40 | mnist = input_data.read_data_sets("MNIST_data/", one_hot=True) 41 | feature_size = 784 42 | num_classes = 10 43 | hidden_size = feature_size - 100 44 | hidden_size2 = hidden_size - 100 45 | batch_size = 50 46 | learning_rate = 1e-4 47 | 48 | 49 | ############################################################### 50 | # BUILD THE GRAPH 51 | ############################################################### 52 | 53 | def create_layer(shape, prev_layer, is_output): 54 | W = tf.Variable(tf.truncated_normal(shape, stddev=0.1)) 55 | b = tf.Variable(tf.constant(0.1, shape=[shape[1]])) 56 | activation = tf.matmul(prev_layer, W) + b 57 | if is_output: 58 | new_layer = tf.nn.softmax(activation) 59 | else: 60 | new_layer = tf.nn.relu(activation) 61 | tf.nn.dropout(new_layer, dropout_prob) 62 | return new_layer 63 | 64 | 65 | # define inputs 66 | x = tf.placeholder(tf.float32, [None, feature_size], name='x-input') 67 | y = tf.placeholder(tf.float32, [None, num_classes], name='y-input') 68 | dropout_prob = tf.placeholder(tf.float32) 69 | 70 | # define layer structure 71 | layer1 = create_layer([feature_size, hidden_size], x, False) 72 | layer2 = create_layer([hidden_size, hidden_size2], layer1, False) 73 | outlayer = create_layer([hidden_size2, num_classes], layer2, True) 74 | prediction = tf.argmax(outlayer, 1) 75 | 76 | # training ops 77 | total_cost = -tf.reduce_sum(y * tf.log(outlayer), reduction_indices=[1]) 78 | mean_cost = tf.reduce_mean(total_cost) 79 | train = tf.train.AdamOptimizer(learning_rate).minimize(mean_cost) 80 | 81 | # accuracy ops 82 | correct_prediction = tf.equal(prediction, tf.argmax(y, 1)) 83 | accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32)) 84 | 85 | ############################################################### 86 | # TRAIN 87 | ############################################################### 88 | 89 | sess = tf.Session() 90 | init = tf.global_variables_initializer() 91 | sess.run(init) 92 | 93 | for i in range(arg_steps): 94 | batch_x, batch_y = mnist.train.next_batch(batch_size) 95 | feed_dict = {x: batch_x, y: batch_y, dropout_prob: 0.5} 96 | sess.run(train, feed_dict=feed_dict) 97 | if i % 100 == 0: 98 | feed_dict = {x: batch_x, y: batch_y, dropout_prob: 0.5} 99 | train_acc = sess.run(accuracy, feed_dict=feed_dict) 100 | print("step %d/%d, training accuracy %g" % (i, arg_steps, train_acc)) 101 | # print final accuracy on test images 102 | feed_dict = {x: mnist.test.images, y: mnist.test.labels, dropout_prob: 1.0} 103 | print (sess.run(accuracy, feed_dict=feed_dict)) 104 | 105 | ############################################################### 106 | # EXPORT TRAINED MODEL 107 | ############################################################### 108 | 109 | # create signature for TensorFlow Serving 110 | tensor_info_x = tf.saved_model.utils.build_tensor_info(x) 111 | tensor_info_pred = tf.saved_model.utils.build_tensor_info(prediction) 112 | tensor_info_scores = tf.saved_model.utils.build_tensor_info(outlayer) 113 | tensor_info_ver = tf.saved_model.utils.build_tensor_info(tf.constant([str(arg_version)])) 114 | prediction_signature = (tf.saved_model.signature_def_utils.build_signature_def( 115 | inputs={'images': tensor_info_x}, 116 | outputs={'prediction': tensor_info_pred, 'scores': tensor_info_scores, 117 | 'model-version': tensor_info_ver}, 118 | method_name=tf.saved_model.signature_constants.PREDICT_METHOD_NAME)) 119 | legacy_init_op = tf.group(tf.tables_initializer(), name='legacy_init_op') 120 | 121 | print("saving model locally") 122 | # save model to disk 123 | export_path = arg_version 124 | builder = tf.saved_model.builder.SavedModelBuilder(export_path) 125 | builder.add_meta_graph_and_variables( 126 | sess, [tf.saved_model.tag_constants.SERVING], 127 | signature_def_map={ 128 | 'predict_images': prediction_signature 129 | }, 130 | legacy_init_op=legacy_init_op) 131 | builder.save() 132 | 133 | ############################################################### 134 | # UPLOAD MODEL TO GOOGLE CLOUD STORAGE 135 | ############################################################### 136 | 137 | print("uploading to " + arg_bucket + "/" + arg_version) 138 | client = storage.Client() 139 | bucket = client.get_bucket(arg_bucket) 140 | if bucket: 141 | for root, dirs, files in os.walk(export_path): 142 | for file in files: 143 | path = os.path.join(root, file) 144 | blob = bucket.blob(path) 145 | blob.upload_from_filename(path) 146 | -------------------------------------------------------------------------------- /tensorflow-model/README.md: -------------------------------------------------------------------------------- 1 | # tensorflow-model 2 | 3 | This folder contains files necessary to build a container for a tf-job 4 | 5 | - MNIST.py 6 | - contains TensorFlow code to train a model and upload the results to a Google Cloud Storage bucket 7 | - Dockerfile 8 | - describes how to build a container to run the job 9 | 10 | Note: before the container can be built, a service account key must be added to this folder that gives us access to the GCS bucket 11 | 12 | 13 | --- 14 | This is not an officially supported Google product 15 | -------------------------------------------------------------------------------- /web-ui/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | MAINTAINER "Daniel Sanche" 3 | 4 | # add TF dependencies 5 | RUN apt-get update && apt-get install -y --no-install-recommends \ 6 | build-essential \ 7 | curl \ 8 | libfreetype6-dev \ 9 | libpng12-dev \ 10 | libzmq3-dev \ 11 | pkg-config \ 12 | python3 \ 13 | python-dev \ 14 | rsync \ 15 | software-properties-common \ 16 | unzip \ 17 | && \ 18 | apt-get clean && \ 19 | rm -rf /var/lib/apt/lists/* 20 | 21 | # add python dependencies 22 | RUN curl -O https://bootstrap.pypa.io/get-pip.py && \ 23 | python get-pip.py && \ 24 | rm get-pip.py 25 | 26 | RUN pip --no-cache-dir install \ 27 | Pillow \ 28 | h5py \ 29 | ipykernel \ 30 | numpy \ 31 | tensorflow \ 32 | tensorflow-serving-api \ 33 | flask \ 34 | && \ 35 | python -m ipykernel.kernelspec 36 | 37 | # show python logs as they occur 38 | ENV PYTHONUNBUFFERED=0 39 | 40 | # add project files 41 | ADD *.py /home/ 42 | ADD templates/* /home/templates/ 43 | ADD static/styles /home/static/styles/ 44 | RUN mkdir /home/static/tmp/ 45 | ADD static/scripts/ /home/static/scripts/ 46 | 47 | # start server on port 5000 48 | WORKDIR /home/ 49 | EXPOSE 5000 50 | ENTRYPOINT python flask_server.py 51 | -------------------------------------------------------------------------------- /web-ui/README.md: -------------------------------------------------------------------------------- 1 | # web-ui 2 | 3 | The files in this folder define a web interface that can be used to interact with a TensorFlow server 4 | 5 | - flask_server.py 6 | - main server code. Handles incoming requests, and renders HTML from template 7 | - mnist_client.py 8 | - code to interact with TensorFlow model server 9 | - takes in an image and server details, and returns the server's response 10 | - Dockerfile 11 | - builds a runnable container out of the files in this directory 12 | 13 | --- 14 | This is not an officially supported Google product 15 | -------------------------------------------------------------------------------- /web-ui/flask_server.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright 2018 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | ''' 16 | 17 | import os 18 | from threading import Timer 19 | import uuid 20 | 21 | from flask import Flask, render_template, request 22 | from mnist_client import get_prediction, random_mnist 23 | 24 | app = Flask(__name__) 25 | 26 | 27 | # handle requests to the server 28 | @app.route("/") 29 | def main(): 30 | # get url parameters for HTML template 31 | name_arg = request.args.get('name', 'mnist-serve') 32 | addr_arg = request.args.get('addr', 'mnist-serve') 33 | port_arg = request.args.get('port', '9000') 34 | args = {"name": name_arg, "addr": addr_arg, "port": port_arg} 35 | print(args) 36 | 37 | output = None 38 | connection = {"text": "", "success": False} 39 | img_id = str(uuid.uuid4()) 40 | img_path = "static/tmp/" + img_id + ".png" 41 | try: 42 | # get a random test MNIST image 43 | x, y, _ = random_mnist(img_path) 44 | # get prediction from TensorFlow server 45 | pred, scores, ver = get_prediction(x, 46 | server_host=addr_arg, 47 | server_port=int(port_arg), 48 | server_name=name_arg, 49 | timeout=10) 50 | # if no exceptions thrown, server connection was a success 51 | connection["text"] = "Connected (model version: " + str(ver) + ")" 52 | connection["success"] = True 53 | # parse class confidence scores from server prediction 54 | scores_dict = [] 55 | for i in range(0, 10): 56 | scores_dict += [{"index": str(i), "val": scores[i]}] 57 | output = {"truth": y, "prediction": pred, 58 | "img_path": img_path, "scores": scores_dict} 59 | except: 60 | # server connection failed 61 | connection["text"] = "Could Not Connect to Server" 62 | # after 10 seconds, delete cached image file from server 63 | t = Timer(10.0, remove_resource, [img_path]) 64 | t.start() 65 | # render results using HTML template 66 | return render_template('index.html', output=output, 67 | connection=connection, args=args) 68 | 69 | 70 | def remove_resource(path): 71 | """ 72 | attempt to delete file from path. Used to clean up MNIST testing images 73 | 74 | :param path: the path of the file to delete 75 | """ 76 | try: 77 | os.remove(path) 78 | print("removed " + path) 79 | except OSError: 80 | print("no file at " + path) 81 | 82 | 83 | if __name__ == '__main__': 84 | app.run(debug=True, host='0.0.0.0') 85 | -------------------------------------------------------------------------------- /web-ui/mnist_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | ''' 3 | Copyright 2018 Google LLC 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ''' 17 | 18 | 19 | from __future__ import print_function 20 | 21 | from grpc.beta import implementations 22 | import numpy as np 23 | from PIL import Image 24 | import tensorflow as tf 25 | from tensorflow.examples.tutorials.mnist import input_data 26 | from tensorflow_serving.apis import predict_pb2 27 | from tensorflow_serving.apis import prediction_service_pb2 28 | 29 | 30 | def get_prediction(image, server_host='127.0.0.1', server_port=9000, 31 | server_name="server", timeout=10.0): 32 | """ 33 | Retrieve a prediction from a TensorFlow model server 34 | 35 | :param image: a MNIST image represented as a 1x784 array 36 | :param server_host: the address of the TensorFlow server 37 | :param server_port: the port used by the server 38 | :param server_name: the name of the server 39 | :param timeout: the amount of time to wait for a prediction to complete 40 | :return 0: the integer predicted in the MNIST image 41 | :return 1: the confidence scores for all classes 42 | :return 2: the version number of the model handling the request 43 | """ 44 | 45 | print("connecting to:%s:%i" % (server_host, server_port)) 46 | # initialize to server connection 47 | channel = implementations.insecure_channel(server_host, server_port) 48 | stub = prediction_service_pb2.beta_create_PredictionService_stub(channel) 49 | 50 | # build request 51 | request = predict_pb2.PredictRequest() 52 | request.model_spec.name = server_name 53 | request.model_spec.signature_name = 'predict_images' 54 | request.inputs['images'].CopyFrom( 55 | tf.contrib.util.make_tensor_proto(image, shape=image.shape)) 56 | 57 | # retrieve results 58 | result = stub.Predict(request, timeout) 59 | resultVal = result.outputs['prediction'].int64_val 60 | scores = result.outputs['scores'].float_val 61 | version = result.outputs['model-version'].string_val 62 | return resultVal[0], scores, version[0] 63 | 64 | 65 | def random_mnist(save_path=None): 66 | """ 67 | Pull a random image out of the MNIST test dataset 68 | Optionally save the selected image as a file to disk 69 | 70 | :param savePath: the path to save the file to. If None, file is not saved 71 | :return 0: a 1x784 representation of the MNIST image 72 | :return 1: the ground truth label associated with the image 73 | :return 2: a bool representing whether the image file was saved to disk 74 | """ 75 | 76 | mnist = input_data.read_data_sets("MNIST_data/", one_hot=True) 77 | batch_size = 1 78 | batch_x, batch_y = mnist.test.next_batch(batch_size) 79 | saved = False 80 | if save_path is not None: 81 | # save image file to disk 82 | try: 83 | data = (batch_x * 255).astype(np.uint8).reshape(28, 28) 84 | img = Image.fromarray(data, 'L') 85 | img.save(save_path) 86 | saved = True 87 | except: 88 | pass 89 | return batch_x, np.argmax(batch_y), saved 90 | -------------------------------------------------------------------------------- /web-ui/static/scripts/material.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * material-design-lite - Material Design Components in CSS, JS and HTML 3 | * @version v1.0.6 4 | * @license Apache-2.0 5 | * @copyright 2015 Google, Inc. 6 | * @link https://github.com/google/material-design-lite 7 | */ 8 | !function(){"use strict";function e(e,t){if(e){if(t.element_.classList.contains(t.CssClasses_.MDL_JS_RIPPLE_EFFECT)){var s=document.createElement("span");s.classList.add(t.CssClasses_.MDL_RIPPLE_CONTAINER),s.classList.add(t.CssClasses_.MDL_JS_RIPPLE_EFFECT);var i=document.createElement("span");i.classList.add(t.CssClasses_.MDL_RIPPLE),s.appendChild(i),e.appendChild(s)}e.addEventListener("click",function(s){s.preventDefault();var i=e.href.split("#")[1],n=t.element_.querySelector("#"+i);t.resetTabState_(),t.resetPanelState_(),e.classList.add(t.CssClasses_.ACTIVE_CLASS),n.classList.add(t.CssClasses_.ACTIVE_CLASS)})}}function t(e,t,s,i){if(i.tabBar_.classList.contains(i.CssClasses_.JS_RIPPLE_EFFECT)){var n=document.createElement("span");n.classList.add(i.CssClasses_.RIPPLE_CONTAINER),n.classList.add(i.CssClasses_.JS_RIPPLE_EFFECT);var a=document.createElement("span");a.classList.add(i.CssClasses_.RIPPLE),n.appendChild(a),e.appendChild(n)}e.addEventListener("click",function(n){n.preventDefault();var a=e.href.split("#")[1],l=i.content_.querySelector("#"+a);i.resetTabState_(t),i.resetPanelState_(s),e.classList.add(i.CssClasses_.IS_ACTIVE),l.classList.add(i.CssClasses_.IS_ACTIVE)})}var s={upgradeDom:function(e,t){},upgradeElement:function(e,t){},upgradeElements:function(e){},upgradeAllRegistered:function(){},registerUpgradedCallback:function(e,t){},register:function(e){},downgradeElements:function(e){}};s=function(){function e(e,t){for(var s=0;sd;d++){if(r=l[d],!r)throw new Error("Unable to find a registered component for the given class.");a.push(r.className),i.setAttribute("data-upgraded",a.join(","));var h=new r.classConstructor(i);h[C]=r,c.push(h);for(var u=0,m=r.callbacks.length;m>u;u++)r.callbacks[u](i);r.widget&&(i[r.className]=h);var E=document.createEvent("Events");E.initEvent("mdl-componentupgraded",!0,!0),i.dispatchEvent(E)}}function a(e){Array.isArray(e)||(e="function"==typeof e.item?Array.prototype.slice.call(e):[e]);for(var t,s=0,i=e.length;i>s;s++)t=e[s],t instanceof HTMLElement&&(n(t),t.children.length>0&&a(t.children))}function l(t){var s="undefined"==typeof t.widget&&"undefined"==typeof t.widget,i=!0;s||(i=t.widget||t.widget);var n={classConstructor:t.constructor||t.constructor,className:t.classAsString||t.classAsString,cssClass:t.cssClass||t.cssClass,widget:i,callbacks:[]};if(p.forEach(function(e){if(e.cssClass===n.cssClass)throw new Error("The provided cssClass has already been registered: "+e.cssClass);if(e.className===n.className)throw new Error("The provided className has already been registered")}),t.constructor.prototype.hasOwnProperty(C))throw new Error("MDL component classes must not have "+C+" defined as a property.");var a=e(t.classAsString,n);a||p.push(n)}function o(t,s){var i=e(t);i&&i.callbacks.push(s)}function r(){for(var e=0;e0&&this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)&&(e.keyCode===this.Keycodes_.UP_ARROW?(e.preventDefault(),t[t.length-1].focus()):e.keyCode===this.Keycodes_.DOWN_ARROW&&(e.preventDefault(),t[0].focus()))}},_.prototype.handleItemKeyboardEvent_=function(e){if(this.element_&&this.container_){var t=this.element_.querySelectorAll("."+this.CssClasses_.ITEM+":not([disabled])");if(t&&t.length>0&&this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)){var s=Array.prototype.slice.call(t).indexOf(e.target);if(e.keyCode===this.Keycodes_.UP_ARROW)e.preventDefault(),s>0?t[s-1].focus():t[t.length-1].focus();else if(e.keyCode===this.Keycodes_.DOWN_ARROW)e.preventDefault(),t.length>s+1?t[s+1].focus():t[0].focus();else if(e.keyCode===this.Keycodes_.SPACE||e.keyCode===this.Keycodes_.ENTER){e.preventDefault();var i=new MouseEvent("mousedown");e.target.dispatchEvent(i),i=new MouseEvent("mouseup"),e.target.dispatchEvent(i),e.target.click()}else e.keyCode===this.Keycodes_.ESCAPE&&(e.preventDefault(),this.hide())}}},_.prototype.handleItemClick_=function(e){e.target.hasAttribute("disabled")?e.stopPropagation():(this.closing_=!0,window.setTimeout(function(e){this.hide(),this.closing_=!1}.bind(this),this.Constant_.CLOSE_TIMEOUT))},_.prototype.applyClip_=function(e,t){this.element_.classList.contains(this.CssClasses_.UNALIGNED)?this.element_.style.clip="":this.element_.classList.contains(this.CssClasses_.BOTTOM_RIGHT)?this.element_.style.clip="rect(0 "+t+"px 0 "+t+"px)":this.element_.classList.contains(this.CssClasses_.TOP_LEFT)?this.element_.style.clip="rect("+e+"px 0 "+e+"px 0)":this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)?this.element_.style.clip="rect("+e+"px "+t+"px "+e+"px "+t+"px)":this.element_.style.clip=""},_.prototype.addAnimationEndListener_=function(){var e=function(){this.element_.removeEventListener("transitionend",e),this.element_.removeEventListener("webkitTransitionEnd",e),this.element_.classList.remove(this.CssClasses_.IS_ANIMATING)}.bind(this);this.element_.addEventListener("transitionend",e),this.element_.addEventListener("webkitTransitionEnd",e)},_.prototype.show=function(e){if(this.element_&&this.container_&&this.outline_){var t=this.element_.getBoundingClientRect().height,s=this.element_.getBoundingClientRect().width;this.container_.style.width=s+"px",this.container_.style.height=t+"px",this.outline_.style.width=s+"px",this.outline_.style.height=t+"px";for(var i=this.Constant_.TRANSITION_DURATION_SECONDS*this.Constant_.TRANSITION_DURATION_FRACTION,n=this.element_.querySelectorAll("."+this.CssClasses_.ITEM),a=0;a=this.maxRows&&e.preventDefault()},E.prototype.onFocus_=function(e){this.element_.classList.add(this.CssClasses_.IS_FOCUSED)},E.prototype.onBlur_=function(e){this.element_.classList.remove(this.CssClasses_.IS_FOCUSED)},E.prototype.updateClasses_=function(){this.checkDisabled(),this.checkValidity(),this.checkDirty()},E.prototype.checkDisabled=function(){this.input_.disabled?this.element_.classList.add(this.CssClasses_.IS_DISABLED):this.element_.classList.remove(this.CssClasses_.IS_DISABLED)},E.prototype.checkDisabled=E.prototype.checkDisabled,E.prototype.checkValidity=function(){this.input_.validity&&(this.input_.validity.valid?this.element_.classList.remove(this.CssClasses_.IS_INVALID):this.element_.classList.add(this.CssClasses_.IS_INVALID))},E.prototype.checkValidity=E.prototype.checkValidity,E.prototype.checkDirty=function(){this.input_.value&&this.input_.value.length>0?this.element_.classList.add(this.CssClasses_.IS_DIRTY):this.element_.classList.remove(this.CssClasses_.IS_DIRTY)},E.prototype.checkDirty=E.prototype.checkDirty,E.prototype.disable=function(){this.input_.disabled=!0,this.updateClasses_()},E.prototype.disable=E.prototype.disable,E.prototype.enable=function(){this.input_.disabled=!1,this.updateClasses_()},E.prototype.enable=E.prototype.enable,E.prototype.change=function(e){this.input_.value=e||"",this.updateClasses_()},E.prototype.change=E.prototype.change,E.prototype.init=function(){if(this.element_&&(this.label_=this.element_.querySelector("."+this.CssClasses_.LABEL),this.input_=this.element_.querySelector("."+this.CssClasses_.INPUT),this.input_)){this.input_.hasAttribute(this.Constant_.MAX_ROWS_ATTRIBUTE)&&(this.maxRows=parseInt(this.input_.getAttribute(this.Constant_.MAX_ROWS_ATTRIBUTE),10),isNaN(this.maxRows)&&(this.maxRows=this.Constant_.NO_MAX_ROWS)),this.boundUpdateClassesHandler=this.updateClasses_.bind(this),this.boundFocusHandler=this.onFocus_.bind(this),this.boundBlurHandler=this.onBlur_.bind(this),this.input_.addEventListener("input",this.boundUpdateClassesHandler),this.input_.addEventListener("focus",this.boundFocusHandler),this.input_.addEventListener("blur",this.boundBlurHandler),this.maxRows!==this.Constant_.NO_MAX_ROWS&&(this.boundKeyDownHandler=this.onKeyDown_.bind(this),this.input_.addEventListener("keydown",this.boundKeyDownHandler));var e=this.element_.classList.contains(this.CssClasses_.IS_INVALID);this.updateClasses_(),this.element_.classList.add(this.CssClasses_.IS_UPGRADED),e&&this.element_.classList.add(this.CssClasses_.IS_INVALID)}},E.prototype.mdlDowngrade_=function(){this.input_.removeEventListener("input",this.boundUpdateClassesHandler),this.input_.removeEventListener("focus",this.boundFocusHandler),this.input_.removeEventListener("blur",this.boundBlurHandler),this.boundKeyDownHandler&&this.input_.removeEventListener("keydown",this.boundKeyDownHandler)},E.prototype.mdlDowngrade=E.prototype.mdlDowngrade_,E.prototype.mdlDowngrade=E.prototype.mdlDowngrade,s.register({constructor:E,classAsString:"MaterialTextfield",cssClass:"mdl-js-textfield",widget:!0});var L=function(e){this.element_=e,this.init()};window.MaterialTooltip=L,L.prototype.Constant_={},L.prototype.CssClasses_={IS_ACTIVE:"is-active"},L.prototype.handleMouseEnter_=function(e){e.stopPropagation();var t=e.target.getBoundingClientRect(),s=t.left+t.width/2,i=-1*(this.element_.offsetWidth/2);0>s+i?(this.element_.style.left=0,this.element_.style.marginLeft=0):(this.element_.style.left=s+"px",this.element_.style.marginLeft=i+"px"),this.element_.style.top=t.top+t.height+10+"px",this.element_.classList.add(this.CssClasses_.IS_ACTIVE),window.addEventListener("scroll",this.boundMouseLeaveHandler,!1),window.addEventListener("touchmove",this.boundMouseLeaveHandler,!1)},L.prototype.handleMouseLeave_=function(e){e.stopPropagation(),this.element_.classList.remove(this.CssClasses_.IS_ACTIVE),window.removeEventListener("scroll",this.boundMouseLeaveHandler),window.removeEventListener("touchmove",this.boundMouseLeaveHandler,!1)},L.prototype.init=function(){if(this.element_){var e=this.element_.getAttribute("for");e&&(this.forElement_=document.getElementById(e)),this.forElement_&&(this.forElement_.hasAttribute("tabindex")||this.forElement_.setAttribute("tabindex","0"),this.boundMouseEnterHandler=this.handleMouseEnter_.bind(this),this.boundMouseLeaveHandler=this.handleMouseLeave_.bind(this),this.forElement_.addEventListener("mouseenter",this.boundMouseEnterHandler,!1),this.forElement_.addEventListener("click",this.boundMouseEnterHandler,!1),this.forElement_.addEventListener("blur",this.boundMouseLeaveHandler),this.forElement_.addEventListener("touchstart",this.boundMouseEnterHandler,!1),this.forElement_.addEventListener("mouseleave",this.boundMouseLeaveHandler))}},L.prototype.mdlDowngrade_=function(){this.forElement_&&(this.forElement_.removeEventListener("mouseenter",this.boundMouseEnterHandler,!1),this.forElement_.removeEventListener("click",this.boundMouseEnterHandler,!1),this.forElement_.removeEventListener("touchstart",this.boundMouseEnterHandler,!1),this.forElement_.removeEventListener("mouseleave",this.boundMouseLeaveHandler))},L.prototype.mdlDowngrade=L.prototype.mdlDowngrade_,L.prototype.mdlDowngrade=L.prototype.mdlDowngrade,s.register({constructor:L,classAsString:"MaterialTooltip",cssClass:"mdl-tooltip"});var I=function(e){this.element_=e,this.init()};window.MaterialLayout=I,I.prototype.Constant_={MAX_WIDTH:"(max-width: 1024px)",TAB_SCROLL_PIXELS:100,MENU_ICON:"menu",CHEVRON_LEFT:"chevron_left",CHEVRON_RIGHT:"chevron_right"},I.prototype.Mode_={STANDARD:0,SEAMED:1,WATERFALL:2,SCROLL:3},I.prototype.CssClasses_={CONTAINER:"mdl-layout__container",HEADER:"mdl-layout__header",DRAWER:"mdl-layout__drawer",CONTENT:"mdl-layout__content",DRAWER_BTN:"mdl-layout__drawer-button",ICON:"material-icons",JS_RIPPLE_EFFECT:"mdl-js-ripple-effect",RIPPLE_CONTAINER:"mdl-layout__tab-ripple-container",RIPPLE:"mdl-ripple",RIPPLE_IGNORE_EVENTS:"mdl-js-ripple-effect--ignore-events",HEADER_SEAMED:"mdl-layout__header--seamed",HEADER_WATERFALL:"mdl-layout__header--waterfall",HEADER_SCROLL:"mdl-layout__header--scroll",FIXED_HEADER:"mdl-layout--fixed-header",OBFUSCATOR:"mdl-layout__obfuscator",TAB_BAR:"mdl-layout__tab-bar",TAB_CONTAINER:"mdl-layout__tab-bar-container",TAB:"mdl-layout__tab",TAB_BAR_BUTTON:"mdl-layout__tab-bar-button",TAB_BAR_LEFT_BUTTON:"mdl-layout__tab-bar-left-button",TAB_BAR_RIGHT_BUTTON:"mdl-layout__tab-bar-right-button",PANEL:"mdl-layout__tab-panel",HAS_DRAWER:"has-drawer",HAS_TABS:"has-tabs",HAS_SCROLLING_HEADER:"has-scrolling-header",CASTING_SHADOW:"is-casting-shadow",IS_COMPACT:"is-compact",IS_SMALL_SCREEN:"is-small-screen",IS_DRAWER_OPEN:"is-visible",IS_ACTIVE:"is-active",IS_UPGRADED:"is-upgraded",IS_ANIMATING:"is-animating",ON_LARGE_SCREEN:"mdl-layout--large-screen-only",ON_SMALL_SCREEN:"mdl-layout--small-screen-only"},I.prototype.contentScrollHandler_=function(){this.header_.classList.contains(this.CssClasses_.IS_ANIMATING)||(this.content_.scrollTop>0&&!this.header_.classList.contains(this.CssClasses_.IS_COMPACT)?(this.header_.classList.add(this.CssClasses_.CASTING_SHADOW),this.header_.classList.add(this.CssClasses_.IS_COMPACT),this.header_.classList.add(this.CssClasses_.IS_ANIMATING)):this.content_.scrollTop<=0&&this.header_.classList.contains(this.CssClasses_.IS_COMPACT)&&(this.header_.classList.remove(this.CssClasses_.CASTING_SHADOW),this.header_.classList.remove(this.CssClasses_.IS_COMPACT),this.header_.classList.add(this.CssClasses_.IS_ANIMATING)))},I.prototype.screenSizeHandler_=function(){this.screenSizeMediaQuery_.matches?this.element_.classList.add(this.CssClasses_.IS_SMALL_SCREEN):(this.element_.classList.remove(this.CssClasses_.IS_SMALL_SCREEN),this.drawer_&&(this.drawer_.classList.remove(this.CssClasses_.IS_DRAWER_OPEN),this.obfuscator_.classList.remove(this.CssClasses_.IS_DRAWER_OPEN)))},I.prototype.drawerToggleHandler_=function(){this.drawer_.classList.toggle(this.CssClasses_.IS_DRAWER_OPEN),this.obfuscator_.classList.toggle(this.CssClasses_.IS_DRAWER_OPEN)},I.prototype.headerTransitionEndHandler_=function(){this.header_.classList.remove(this.CssClasses_.IS_ANIMATING)},I.prototype.headerClickHandler_=function(){this.header_.classList.contains(this.CssClasses_.IS_COMPACT)&&(this.header_.classList.remove(this.CssClasses_.IS_COMPACT),this.header_.classList.add(this.CssClasses_.IS_ANIMATING))},I.prototype.resetTabState_=function(e){for(var t=0;tn;n++){var a=s[n];a.classList&&a.classList.contains(this.CssClasses_.HEADER)&&(this.header_=a),a.classList&&a.classList.contains(this.CssClasses_.DRAWER)&&(this.drawer_=a),a.classList&&a.classList.contains(this.CssClasses_.CONTENT)&&(this.content_=a)}this.header_&&(this.tabBar_=this.header_.querySelector("."+this.CssClasses_.TAB_BAR));var l=this.Mode_.STANDARD;if(this.header_&&(this.header_.classList.contains(this.CssClasses_.HEADER_SEAMED)?l=this.Mode_.SEAMED:this.header_.classList.contains(this.CssClasses_.HEADER_WATERFALL)?(l=this.Mode_.WATERFALL,this.header_.addEventListener("transitionend",this.headerTransitionEndHandler_.bind(this)),this.header_.addEventListener("click",this.headerClickHandler_.bind(this))):this.header_.classList.contains(this.CssClasses_.HEADER_SCROLL)&&(l=this.Mode_.SCROLL,e.classList.add(this.CssClasses_.HAS_SCROLLING_HEADER)),l===this.Mode_.STANDARD?(this.header_.classList.add(this.CssClasses_.CASTING_SHADOW),this.tabBar_&&this.tabBar_.classList.add(this.CssClasses_.CASTING_SHADOW)):l===this.Mode_.SEAMED||l===this.Mode_.SCROLL?(this.header_.classList.remove(this.CssClasses_.CASTING_SHADOW),this.tabBar_&&this.tabBar_.classList.remove(this.CssClasses_.CASTING_SHADOW)):l===this.Mode_.WATERFALL&&(this.content_.addEventListener("scroll",this.contentScrollHandler_.bind(this)),this.contentScrollHandler_())),this.drawer_){var o=this.element_.querySelector("."+this.CssClasses_.DRAWER_BTN);if(!o){o=document.createElement("div"),o.classList.add(this.CssClasses_.DRAWER_BTN);var r=document.createElement("i");r.classList.add(this.CssClasses_.ICON),r.textContent=this.Constant_.MENU_ICON,o.appendChild(r)}this.drawer_.classList.contains(this.CssClasses_.ON_LARGE_SCREEN)?o.classList.add(this.CssClasses_.ON_LARGE_SCREEN):this.drawer_.classList.contains(this.CssClasses_.ON_SMALL_SCREEN)&&o.classList.add(this.CssClasses_.ON_SMALL_SCREEN),o.addEventListener("click",this.drawerToggleHandler_.bind(this)),this.element_.classList.add(this.CssClasses_.HAS_DRAWER),this.element_.classList.contains(this.CssClasses_.FIXED_HEADER)?this.header_.insertBefore(o,this.header_.firstChild):this.element_.insertBefore(o,this.content_);var d=document.createElement("div");d.classList.add(this.CssClasses_.OBFUSCATOR),this.element_.appendChild(d),d.addEventListener("click",this.drawerToggleHandler_.bind(this)),this.obfuscator_=d}if(this.screenSizeMediaQuery_=window.matchMedia(this.Constant_.MAX_WIDTH),this.screenSizeMediaQuery_.addListener(this.screenSizeHandler_.bind(this)),this.screenSizeHandler_(),this.header_&&this.tabBar_){this.element_.classList.add(this.CssClasses_.HAS_TABS);var _=document.createElement("div");_.classList.add(this.CssClasses_.TAB_CONTAINER),this.header_.insertBefore(_,this.tabBar_),this.header_.removeChild(this.tabBar_);var h=document.createElement("div");h.classList.add(this.CssClasses_.TAB_BAR_BUTTON),h.classList.add(this.CssClasses_.TAB_BAR_LEFT_BUTTON);var p=document.createElement("i");p.classList.add(this.CssClasses_.ICON),p.textContent=this.Constant_.CHEVRON_LEFT,h.appendChild(p),h.addEventListener("click",function(){this.tabBar_.scrollLeft-=this.Constant_.TAB_SCROLL_PIXELS}.bind(this));var c=document.createElement("div");c.classList.add(this.CssClasses_.TAB_BAR_BUTTON),c.classList.add(this.CssClasses_.TAB_BAR_RIGHT_BUTTON);var u=document.createElement("i");u.classList.add(this.CssClasses_.ICON),u.textContent=this.Constant_.CHEVRON_RIGHT,c.appendChild(u),c.addEventListener("click",function(){this.tabBar_.scrollLeft+=this.Constant_.TAB_SCROLL_PIXELS}.bind(this)),_.appendChild(h),_.appendChild(this.tabBar_),_.appendChild(c);var C=function(){this.tabBar_.scrollLeft>0?h.classList.add(this.CssClasses_.IS_ACTIVE):h.classList.remove(this.CssClasses_.IS_ACTIVE),this.tabBar_.scrollLeft0)return;this.setFrameCount(1);var i,n,a=e.currentTarget.getBoundingClientRect();if(0===e.clientX&&0===e.clientY)i=Math.round(a.width/2),n=Math.round(a.height/2);else{var l=e.clientX?e.clientX:e.touches[0].clientX,o=e.clientY?e.clientY:e.touches[0].clientY;i=Math.round(l-a.left),n=Math.round(o-a.top)}this.setRippleXY(i,n),this.setRippleStyles(!0),window.requestAnimationFrame(this.animFrameHandler.bind(this))}},b.prototype.upHandler_=function(e){e&&2!==e.detail&&this.rippleElement_.classList.remove(this.CssClasses_.IS_VISIBLE),window.setTimeout(function(){this.rippleElement_.classList.remove(this.CssClasses_.IS_VISIBLE)}.bind(this),0)},b.prototype.init=function(){if(this.element_){var e=this.element_.classList.contains(this.CssClasses_.RIPPLE_CENTER);this.element_.classList.contains(this.CssClasses_.RIPPLE_EFFECT_IGNORE_EVENTS)||(this.rippleElement_=this.element_.querySelector("."+this.CssClasses_.RIPPLE),this.frameCount_=0,this.rippleSize_=0,this.x_=0,this.y_=0,this.ignoringMouseDown_=!1,this.boundDownHandler=this.downHandler_.bind(this),this.element_.addEventListener("mousedown",this.boundDownHandler),this.element_.addEventListener("touchstart",this.boundDownHandler),this.boundUpHandler=this.upHandler_.bind(this),this.element_.addEventListener("mouseup",this.boundUpHandler),this.element_.addEventListener("mouseleave",this.boundUpHandler),this.element_.addEventListener("touchend",this.boundUpHandler),this.element_.addEventListener("blur",this.boundUpHandler),this.getFrameCount=function(){return this.frameCount_},this.setFrameCount=function(e){this.frameCount_=e},this.getRippleElement=function(){return this.rippleElement_},this.setRippleXY=function(e,t){this.x_=e,this.y_=t},this.setRippleStyles=function(t){if(null!==this.rippleElement_){var s,i,n,a="translate("+this.x_+"px, "+this.y_+"px)";t?(i=this.Constant_.INITIAL_SCALE,n=this.Constant_.INITIAL_SIZE):(i=this.Constant_.FINAL_SCALE,n=this.rippleSize_+"px",e&&(a="translate("+this.boundWidth/2+"px, "+this.boundHeight/2+"px)")),s="translate(-50%, -50%) "+a+i,this.rippleElement_.style.webkitTransform=s,this.rippleElement_.style.msTransform=s,this.rippleElement_.style.transform=s,t?this.rippleElement_.classList.remove(this.CssClasses_.IS_ANIMATING):this.rippleElement_.classList.add(this.CssClasses_.IS_ANIMATING)}},this.animFrameHandler=function(){this.frameCount_-->0?window.requestAnimationFrame(this.animFrameHandler.bind(this)):this.setRippleStyles(!1)})}},b.prototype.mdlDowngrade_=function(){this.element_.removeEventListener("mousedown",this.boundDownHandler),this.element_.removeEventListener("touchstart",this.boundDownHandler),this.element_.removeEventListener("mouseup",this.boundUpHandler),this.element_.removeEventListener("mouseleave",this.boundUpHandler),this.element_.removeEventListener("touchend",this.boundUpHandler),this.element_.removeEventListener("blur",this.boundUpHandler)},b.prototype.mdlDowngrade=b.prototype.mdlDowngrade_,b.prototype.mdlDowngrade=b.prototype.mdlDowngrade,s.register({constructor:b,classAsString:"MaterialRipple",cssClass:"mdl-js-ripple-effect",widget:!1})}(); 10 | //# sourceMappingURL=material.min.js.map 11 | -------------------------------------------------------------------------------- /web-ui/static/styles/demo.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | html, body { 18 | font-family: 'Roboto', 'Helvetica', sans-serif; 19 | margin: 0; 20 | padding: 0; 21 | } 22 | .mdl-demo .mdl-layout__header-row { 23 | padding-left: 40px; 24 | } 25 | .mdl-demo .mdl-layout.is-small-screen .mdl-layout__header-row h3 { 26 | font-size: inherit; 27 | } 28 | .mdl-demo .mdl-layout__tab-bar-button { 29 | display: none; 30 | } 31 | .mdl-demo .mdl-layout.is-small-screen .mdl-layout__tab-bar .mdl-button { 32 | display: none; 33 | } 34 | .mdl-demo .mdl-layout:not(.is-small-screen) .mdl-layout__tab-bar, 35 | .mdl-demo .mdl-layout:not(.is-small-screen) .mdl-layout__tab-bar-container { 36 | overflow: visible; 37 | } 38 | .mdl-demo .mdl-layout__tab-bar-container { 39 | height: 64px; 40 | } 41 | .mdl-demo .mdl-layout__tab-bar { 42 | padding: 0; 43 | padding-left: 16px; 44 | box-sizing: border-box; 45 | height: 100%; 46 | width: 100%; 47 | } 48 | .mdl-demo .mdl-layout__tab-bar .mdl-layout__tab { 49 | height: 64px; 50 | line-height: 64px; 51 | } 52 | .mdl-demo .mdl-layout__tab-bar .mdl-layout__tab.is-active::after { 53 | background-color: white; 54 | height: 4px; 55 | } 56 | .mdl-demo main > .mdl-layout__tab-panel { 57 | padding: 8px; 58 | padding-top: 48px; 59 | } 60 | .mdl-demo .mdl-card { 61 | height: auto; 62 | display: -webkit-flex; 63 | display: -ms-flexbox; 64 | display: flex; 65 | -webkit-flex-direction: column; 66 | -ms-flex-direction: column; 67 | flex-direction: column; 68 | } 69 | .mdl-demo .mdl-card > * { 70 | height: auto; 71 | } 72 | .mdl-demo .mdl-card .mdl-card__supporting-text { 73 | margin: 40px; 74 | -webkit-flex-grow: 1; 75 | -ms-flex-positive: 1; 76 | flex-grow: 1; 77 | padding: 0; 78 | color: inherit; 79 | width: calc(100% - 80px); 80 | } 81 | .mdl-demo.mdl-demo .mdl-card__supporting-text h4 { 82 | margin-top: 0; 83 | margin-bottom: 20px; 84 | } 85 | .mdl-demo .mdl-card__actions { 86 | margin: 0; 87 | padding: 4px 40px; 88 | color: inherit; 89 | } 90 | .mdl-demo .mdl-card__actions a { 91 | color: #00BCD4; 92 | margin: 0; 93 | } 94 | .mdl-demo .mdl-card__actions a:hover, 95 | .mdl-demo .mdl-card__actions a:active { 96 | color: inherit; 97 | background-color: transparent; 98 | } 99 | .mdl-demo .mdl-card__supporting-text + .mdl-card__actions { 100 | border-top: 1px solid rgba(0, 0, 0, 0.12); 101 | } 102 | .mdl-demo #add { 103 | position: absolute; 104 | right: 40px; 105 | top: 36px; 106 | z-index: 999; 107 | } 108 | 109 | .mdl-demo .mdl-layout__content section:not(:last-of-type) { 110 | position: relative; 111 | margin-bottom: 48px; 112 | } 113 | .mdl-demo section.section--center { 114 | max-width: 860px; 115 | } 116 | .mdl-demo #features section.section--center { 117 | max-width: 620px; 118 | } 119 | .mdl-demo section > header{ 120 | display: -webkit-flex; 121 | display: -ms-flexbox; 122 | display: flex; 123 | -webkit-align-items: center; 124 | -ms-flex-align: center; 125 | align-items: center; 126 | -webkit-justify-content: center; 127 | -ms-flex-pack: center; 128 | justify-content: center; 129 | } 130 | .mdl-demo section > .section__play-btn { 131 | min-height: 200px; 132 | } 133 | .mdl-demo section > header > .material-icons { 134 | font-size: 3rem; 135 | } 136 | .mdl-demo section > button { 137 | position: absolute; 138 | z-index: 99; 139 | top: 8px; 140 | right: 8px; 141 | } 142 | .mdl-demo section .section__circle { 143 | display: -webkit-flex; 144 | display: -ms-flexbox; 145 | display: flex; 146 | -webkit-align-items: center; 147 | -ms-flex-align: center; 148 | align-items: center; 149 | -webkit-justify-content: flex-start; 150 | -ms-flex-pack: start; 151 | justify-content: flex-start; 152 | -webkit-flex-grow: 0; 153 | -ms-flex-positive: 0; 154 | flex-grow: 0; 155 | -webkit-flex-shrink: 1; 156 | -ms-flex-negative: 1; 157 | flex-shrink: 1; 158 | } 159 | .mdl-demo section .section__text { 160 | -webkit-flex-grow: 1; 161 | -ms-flex-positive: 1; 162 | flex-grow: 1; 163 | -webkit-flex-shrink: 0; 164 | -ms-flex-negative: 0; 165 | flex-shrink: 0; 166 | padding-top: 8px; 167 | } 168 | .mdl-demo section .section__text h5 { 169 | font-size: inherit; 170 | margin: 0; 171 | margin-bottom: 0.5em; 172 | } 173 | .mdl-demo section .section__text a { 174 | text-decoration: none; 175 | } 176 | .mdl-demo section .section__circle-container > .section__circle-container__circle { 177 | width: 64px; 178 | height: 64px; 179 | border-radius: 32px; 180 | margin: 8px 0; 181 | } 182 | .mdl-demo section.section--footer .section__circle--big { 183 | width: 100px; 184 | height: 100px; 185 | border-radius: 50px; 186 | margin: 8px 32px; 187 | } 188 | .mdl-demo .is-small-screen section.section--footer .section__circle--big { 189 | width: 50px; 190 | height: 50px; 191 | border-radius: 25px; 192 | margin: 8px 16px; 193 | } 194 | .mdl-demo section.section--footer { 195 | padding: 64px 0; 196 | margin: 0 -8px -8px -8px; 197 | } 198 | .mdl-demo section.section--center .section__text:not(:last-child) { 199 | border-bottom: 1px solid rgba(0,0,0,.13); 200 | } 201 | .mdl-demo .mdl-card .mdl-card__supporting-text > h3:first-child { 202 | margin-bottom: 24px; 203 | } 204 | .mdl-demo .mdl-layout__tab-panel:not(#overview) { 205 | background-color: white; 206 | } 207 | .mdl-demo #features section { 208 | margin-bottom: 72px; 209 | } 210 | .mdl-demo #features h4, #features h5 { 211 | margin-bottom: 16px; 212 | } 213 | .mdl-demo .toc { 214 | border-left: 4px solid #C1EEF4; 215 | margin: 24px; 216 | padding: 0; 217 | padding-left: 8px; 218 | display: -webkit-flex; 219 | display: -ms-flexbox; 220 | display: flex; 221 | -webkit-flex-direction: column; 222 | -ms-flex-direction: column; 223 | flex-direction: column; 224 | } 225 | .mdl-demo .toc h4 { 226 | font-size: 0.9rem; 227 | margin-top: 0; 228 | } 229 | .mdl-demo .toc a { 230 | color: #4DD0E1; 231 | text-decoration: none; 232 | font-size: 16px; 233 | line-height: 28px; 234 | display: block; 235 | } 236 | .mdl-demo .mdl-menu__container { 237 | z-index: 99; 238 | } 239 | -------------------------------------------------------------------------------- /web-ui/templates/index.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Kubeflow UI 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
32 |
33 |
34 |

Kubeflow Codelab UI

35 |
36 |
37 |
38 | 39 |
40 |
41 |
42 |

MNIST Model Server

43 |
44 |
45 | 46 | 47 |
48 |
49 | 50 | 51 |
52 |
53 | 56 | 57 | Input is not a valid port 58 |
59 | 60 |
61 | {% if connection.success %} 62 |
✓ {{ connection.text }}
63 | {% else %} 64 |
❗ {{ connection.text }}
65 | {% endif %} 66 |
67 |
68 |
69 | 70 | {% if output %} 71 |
72 |
73 |
74 |

Test Results

75 | 77 |

78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | {% for score in output.scores %} 89 | 90 | 91 | 100 | 101 | {% endfor %} 102 | 103 |
Truth{{ output.truth }}
Prediction {{ output.prediction }}
Probability {{ score.index }}: 92 |
95 | 99 |
104 |

105 | 108 |
109 |
110 |
111 | {% endif %} 112 |
113 |
114 | 115 | 116 | --------------------------------------------------------------------------------